client-side-databases: the same Angular chat app rebuilt on eight local-first databases
An implementation of the exact same app in Firestore, AWS Datastore, PouchDB, RxDB and WatermelonDB
At a glance
- What is it?
- The pubkey/client-side-databases repository implements one identical chat application on eight local-first database stacks, from Firestore and AWS Datastore to RxDB, PouchDB, WatermelonDB, PGlite and SQLite compiled to WebAssembly, then measures them all with the same browser test. It suits engineers choosing a client-side database who want comparable numbers rather than vendor benchmarks, and its most useful output is the explanation of why each number is what it is.
- Who is it for?
- client-side-databases fits an engineer choosing between a local-first database stack who wants the same application implemented on each candidate and measured the same way, since the identical chat app removes the usual excuse that a slow number came from the surrounding code.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One chat application, eight database backends
The premise of this repository is simple and unusually disciplined: the exact same chat application has been implemented with different local-first database technologies, so the differences can be compared.
The application is a web based Angular application with functionality similar to Whatsapp Web. The same application means the same chat UI, the same insert patterns, the same search behaviour, which is the point. When a database is slower in a benchmark, the reason is the database rather than a different amount of surrounding work.
The implemented stacks are listed in the README. AWS Amplify Datastore and Firebase Firestore are the hosted, server-backed options. PouchDB appears with the IndexedDB adapter and CouchDB replication. RxDB appears twice, once with LocalStorage storage and GraphQL replication and once with Dexie.js storage and GraphQL replication. WatermelonDB comes with the LokiJS adapter. PGlite, which is Postgres in WebAssembly, is configured with IndexedDB persistence and live queries. SQLite Wasm is used with the OPFS SyncAccessHandle Pool VFS running in a WebWorker.
Three of those entries carry an explicit caveat that there is no backend sync at the moment: WatermelonDB, PGlite and SQLite Wasm. That is a detail worth holding on to when reading the metrics, because a local-only stack and a local-first stack with replication are not the same product, and a comparison that quietly omits replication measures a different thing in one column than in the others.
The repository is a TypeScript project with an Angular build, and the top-level layout shows a projects/ directory holding the per-database Angular projects, an orga/ directory, a scripts/ directory for the measurement tooling, a test/ directory, a firebase.json, a .firebaserc, and an angular.json. The package.json is named blueprint, at version 0.0.0, and every script in it is repeated once per database, which is the clearest evidence of the whole design.
A generated metrics table, and the script that regenerates it
The metrics are measured automatically via code in browser tests running chrome:headless, and the README is upfront about how to read them: the results heavily depend on the developer's device, and you should compare the values relative to another rather than as absolute values. It also suggests creating new metrics that better represent how you would use the respective database.
That caveat is the most important line in the whole document, and it is the reason the project is useful. A single number on one machine says little. Eight databases measured by the same code on the same machine say a great deal, because the differences between them are far larger than the noise from the hardware.
The results can be reproduced by running a single command in the root folder:
sh measure-metrics.shThe table itself is generated rather than hand-maintained, and the README carries a comment saying not to edit the table directly because it is generated by ./scripts/aggregate-metrics.mjs. That comment is a small piece of engineering discipline that says a lot: the numbers in the README are reproducible artefacts, not notes taken on a particular day.
The metrics cover a wide range of behaviour, and each has a definition in the Metrics Explanation section. Bundle size, plain JavaScript is the full bundle including WebWorker code without minification or gzip, and the second size metric adds the WebAssembly files and PGlite's data bundle after minification and gzip. The timing metrics cover page load time, which is downloading and parsing the bundle, the first Angular component render, and the first full render, which is when all relevant data is displayed. The insert metrics cover a single message, twenty messages in serial and twenty in parallel, and the two rendering metrics measure how long a new message takes to reach the DOM and how long a user change takes to show a new message list. Search query time is measured by regex and like operator, and storage usage is the size of the stored data in IndexedDB, OPFS and localStorage after the full test dataset is inserted.
That last metric deserves attention. It is the one that reflects what a user pays for, in a browser, in a device that does not have room to spare.
Reading the outliers: LokiJS, Firebase and PouchDB
The Investigations section is where the repository earns its keep, because it explains the causes of the numbers rather than leaving them as a table to be interpreted.
WatermelonDB is fast because it uses LokiJS as storage, which is an in-memory database that regularly persists to IndexedDB either on an interval or when the browser tab is closed. The point of the arrangement is that fewer slow IndexedDB transactions are used. The README is careful to price that benefit, listing the downsides: initial page load is much slower when much data is already stored, because all data must be loaded before any database operation can be done; all data must fit into memory; data can be lost when the JavaScript process is killed ungracefully, such as a browser crash or power loss; and there is no multi-tab support with plain LokiJS, because the data is not shared between multiple tabs of the same origin.
Those four bullets are a complete description of when not to use it, and they line up with the table: WatermelonDB posts the fastest insert times of the group, at 4ms for one message and 94ms for twenty in serial, and also the smallest storage usage of the group at 1256kb, but its first full render with many messages is 166ms, which is fine, while the in-memory model is what would hurt an app with a large dataset.
Firebase is slow on first render for a different reason. On the first page load, Firebase ensures that the local data is equal to the server side state, which means the client has to be online at application startup, which is why the README says Firebase is not completely offline first. Several requests to the backend are needed before the database will respond to queries, and it becomes even slower the more data exists and has to be validated. The table bears that out: 3949ms for inserting twenty messages in serial, and 2520ms in parallel, the slowest figures in the insert rows by an order of magnitude.
PouchDB is slow for a reason that is about the benchmark rather than the database. The README says that for PouchDB and for RxDB with PouchDB storage, the old IndexedDB adapter was used, that it is much less optimized than the new adapter, but that the new one made problems with returning the correct results, so the old one was kept for the measurement.
Bundle size is where the hosted and the WebAssembly options differ
The bundle size rows show the cost of each approach before any data exists, and they split the eight stacks into clear groups.
On the plain JavaScript measure, the range is narrow: PouchDB at 911kb, SQLite Wasm at 969kb, RxDB with LocalStorage at 988kb, WatermelonDB at 1055kb, RxDB with Dexie at 1088kb, Firebase at 1083kb, PGlite at 1393kb, and AWS Amplify at 1954kb. That is roughly a two to one spread across the whole set, which is already more useful than most vendor comparisons offer.
The second measure, which adds WebAssembly and gzips everything, tells a much more dramatic story. PouchDB comes to 223kb, WatermelonDB 245kb, RxDB with LocalStorage 247kb, RxDB with Dexie 280kb, Firebase 272kb, and AWS Amplify 455kb. Then PGlite jumps to 6044kb and SQLite Wasm to 641kb, because those two carry an actual database engine as WebAssembly, and in PGlite's case the data bundle as well.
So the Postgres-in-the-browser option costs roughly twenty times the bundle of the PouchDB option, and the SQLite Wasm option costs roughly three times. That is the price of running a real SQL engine in a browser, and it is the kind of number that a design discussion about local-first architecture needs and rarely gets.
The storage row completes the picture. AWS Amplify uses 105kb of local storage, RxDB with LocalStorage 727kb, SQLite Wasm 5286kb, Firebase 884kb, RxDB with Dexie 1344kb, WatermelonDB 1256kb, PouchDB 1569kb, and PGlite 15226kb. The two WebAssembly engines are also the two that store the most data locally, which follows from storing a database rather than a document.
The caveat about device dependence applies more to the timing rows than to the size rows, since a bundle is a bundle regardless of the machine. Comparing relative numbers is what the README asks for.
How the build and the measurement harness are wired
The package.json shows the repetition that makes the comparison work, and it also shows how each database is built and analysed.
There is one build target per stack, and each one is an Angular production build with ahead-of-time compilation and a stats JSON file emitted, for example the aws, firebase, pouchdb, rxdb-localstorage, rxdb-dexie, watermelondb and pglite variants, each with its own --project argument. The aggregate build target removes the dist directory first and then runs all of the per-stack builds.
There is a matching set of analysis targets, one per stack, and each runs webpack-bundle-analyzer against the stats file for that stack in the dist directory. That is how the bundle size numbers in the table are produced, from the real build artefacts rather than from an estimate.
The postinstall script is a two-step chain. It runs a script called generate-example-data, which invokes a TypeScript file with ts-node, and the name says exactly what it does: it generates the example data the tests operate on. The metrics are measured against a known dataset, which is what makes the numbers comparable across databases.
The repository root has a .browserslistrc, an .nvmrc and an .npmrc, plus a .eslintrc.json, a renovate.json for dependency updates, a firebase.json and a .firebaserc, since two of the stacks are hosted Firebase and AWS services and need their project configuration to run at all. There is also a docs/ directory and an orga/ directory, the latter holding the chat application assets.
The CI side of the repository is in .github/, and the whole structure is a monorepo of Angular projects that share one codebase and differ only in the persistence layer, which is the only way to get a comparison you can actually trust.
Licence, age of the results, and what this repository is not
The licence is Apache-2.0, which is a permissive grant with an explicit patent clause, and the LICENSE file is at the root of the tree. For a repository of example applications and measurement scripts, that licence means the code can be used as a starting point in commercial work without obligation beyond attribution and notice.
The dates need attention. The repository is not archived and the last push was on 2026-10-16, on the master branch, so the code is being worked on. But the only release tag is 1.0.0, from 2021-11-06, and its description is Added RxDB+LokiJS, which tells you that the set of databases has changed considerably since then. PGlite and SQLite Wasm are not in that tag.
That gap between a current commit and a five-year-old release tag is the thing to keep in mind when reading the metrics table. The versions of the databases under test are whatever the current dependencies resolve to, and the table does not pin them in the visible text. The metric definitions are careful, the measurement is automated, and the table is generated, but the specific versions behind a given number are not recorded in the README, so a number from a year ago and a number from today are not strictly comparable.
The last thing to say plainly is what this repository is not. It is not a client-side database, and it does not implement a storage engine. It is a comparison harness and a body of measurements, with the code written so that a reader can reproduce the numbers and understand the causes. The README even suggests creating new metrics that better represent how you would use the respective database, which is an invitation to treat the harness as a starting point rather than a finished benchmark.
That framing is also why the Investigations section matters more than the table. The table tells you which stack was faster on one machine. The Investigations tell you why, and the why is what survives the next version bump.
Editorial conclusion
client-side-databases fits an engineer choosing between a local-first database stack who wants the same application implemented on each candidate and measured the same way, since the identical chat app removes the usual excuse that a slow number came from the surrounding code. It is the wrong thing to treat as a benchmark result in absolute terms, because the README states the values depend heavily on the developer's device and should be compared relatively, and it is the wrong thing to copy as a production architecture, since several of the stacks are configured without backend sync. Before you rely on the table, read the Investigations section, which explains the cause of most of the outliers, and reproduce the numbers yourself with the measure-metrics.sh script on the hardware you plan to ship on.
Frequently asked questions
How do I reproduce the metrics in the pubkey client-side-databases repository?
The README says you can reproduce the values by running `sh measure-metrics.sh` in the root folder. The metrics are measured automatically via code in browser tests running chrome:headless, and the table in the README is generated by ./scripts/aggregate-metrics.mjs rather than edited by hand.
Which databases does the client-side-databases comparison cover?
AWS Amplify Datastore, Firebase Firestore, PouchDB with the IndexedDB adapter and CouchDB replication, RxDB with LocalStorage storage and with Dexie.js storage, both using GraphQL replication, WatermelonDB with the LokiJS adapter, PGlite, and SQLite Wasm. The README notes that WatermelonDB, PGlite and SQLite Wasm have no backend sync at the moment.
Why is WatermelonDB fast in the metrics table?
It uses the LokiJS database as storage, which is an in-memory database that regularly persists to IndexedDB either on an interval or when the tab is closed, so fewer slow IndexedDB transactions are used. The README lists the costs: slower initial load with much stored data, all data must fit in memory, data can be lost if the browser crashes, and there is no multi-tab support with plain LokiJS.
Why is Firebase slow on first render?
The README explains that on first page load Firebase ensures the local data equals the server side state, so the client has to be online at startup and Firebase is therefore not completely offline first. Several requests to the backend are needed before the database responds to queries, and it gets slower the more data exists to validate.
What licence does the client-side-databases repository use?
The licence field on the repository says Apache-2.0, and the LICENSE file is at the root of the tree. The repository is not archived, with the last push on 2026-10-16 and a single release tag, 1.0.0, from 2021-11-06.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pubkey-client-side-databases)Was this useful?