1.118.1 Local-First Sync & CRDT Libraries#
Libraries that keep data editable offline on each device and reconcile it on reconnect: CRDT libraries and sync engines over a database, compared on conflict model, server requirement, offline writes, authorization, bundle size, license and stall test.
Evidence: measured, method not yet published. The numbers here were produced by probes run on a stated machine; the harness is not in the public repository yet, so a reader cannot re-run them.
At a glance#
| Library | Best for | Verdict | Latest release |
|---|---|---|---|
| Yjs | Collaborative text with the widest editor bindings | Healthy; 130 commits since 2026-03-01 | 13.6.33 · 2026-09-23 |
| Automerge | Nested JSON documents with history | Healthy; automerge-repo’s npm latest tag is an alpha | 0.1.2 · 2026-04-15 |
| Loro | Structured documents and history browsing | Healthy; youngest (2023) | 1.16.2 · 2026-09-21 |
| Diamond Types | Reference implementation of a fast text CRDT | Live source, stale packages, text only | 1.0.2 · 2023-05-15 |
| ElectricSQL | Postgres shapes streamed to clients with a write path you own | Very active; Apache-2.0 | 1.5.28 · 2026-09-09 |
| PowerSync | Offline write queue to an existing database | Very active; service license FSL-1.1-ALv2 | 0.10.2 · 2026-10-01 |
| Zero | Online-first apps wanting instant UI | Active; rejects writes while disconnected | 1.9.0 · 2026-10-05 |
| Replicache | Existing installations | Maintenance mode by announcement | 15.3.0 · 2025-07-02 |
| Triplit | Not for new work | Stalled; AGPL-3.0; co-founder joined Supabase 2025-10-08 | 1.0.50 · 2025-07-31 |
| Jazz | Single-vendor sync with groups or row-level permissions | Very active, in transition between two APIs | 1.2.0 · 2026-08-22 |
| InstantDB | Hosted or self-hosted app database with permissions | Active; offline-write behavior not confirmed | 1.0.67 · 2026-08-31 |
| RxDB replication | Replicating to an HTTP, GraphQL or CouchDB backend you have | Very active; premium boundary to confirm | 17.6.0 · 2026-10-05 |
| PouchDB replication | CouchDB backends | Alive but slow; last npm release 2024-06-21 | 9.0.0 · 2024-06-21 |
Latest release observed from npm and PyPI in 2026-10.
What the research found
- Zero’s documentation says writes are rejected while the client is disconnected, error or needs-auth, and queued only in the short connecting state. Its authors call offline writes a non-priority. — A sync engine is not an offline-write engine. Check the documentation for this before choosing; Zero is out for offline edits.
- Yjs, Automerge and Loro converged on every merge probe run: 200 seeded trials each, three replicas, 12 edits per replica, with no lost inserts. Same-key writes resolved to one value everywhere. — Choose among the CRDT libraries on bytes, data model and ecosystem, not on whether they merge.
- Yjs is 23.7 kB gzipped as one JavaScript file. Loro and Automerge are 12.9 and 15.3 kB of JavaScript plus a WebAssembly file near 1.1 MB gzipped. PowerSync’s client is 159 kB gzipped plus a SQLite build. — Bundle cost splits the CRDT family by language of the core, and sets a floor on engines that bring a local database.
- Triplit has 4 open pull requests, 0 merged and 0 commits since 2026-03-01 and its co-founder joined Supabase on 2025-10-08; Replicache is in maintenance mode; Diamond Types has 2023 npm packages; PouchDB’s last release is 2024-06-21. — Four of thirteen options fail a new-work durability check. Triplit is stalled and under AGPL-3.0.
- Authorization on synced data is absent from every CRDT library, left to your proxy in Electric, declared by rules in PowerSync, CEL rules in InstantDB and context in Zero. PowerSync’s service license is FSL-1.1-ALv2, not an OSI license. — Permissions and license terms decide fit more often than conflict handling does.
What the research recommends
Default — Choose the conflict model first (merge rule, server referee, or last writer wins), then the server you are willing to run. Treat offline-write support as a pass/fail check.
| Persona | Pick |
|---|---|
| collaborative document editor | Yjs by default; Loro for trees and history; Automerge for JSON-first documents. Avoid row-based server-authoritative engines. |
| field service app flaky networks | PowerSync (Postgres, MongoDB, MySQL, SQL Server), PouchDB with CouchDB, or RxDB replication to an existing API. Avoid Zero, Triplit and Replicache. |
| multi device notes todo app | CRDT documents with a thin relay (Yjs, Loro or Automerge); InstantDB or Jazz if hosted is acceptable. |
| team dashboard offline edits | PowerSync or ElectricSQL with an explicit write path, or RxDB replication; Zero only if users are online nearly always. Avoid CRDT-only stacks. |
bundle size gzip bytes
- loro crdt js: 12948
- automerge js slim: 15273
- electric client: 18019
- yjs: 23735
- rxdb core: 41733
- pouchdb browser: 43095
- instantdb core: 44375
- rxdb core replication: 54953
- triplit client: 72175
- zero client: 95795
- powersync web: 159410
- jazz tools classic: 410827
wasm gzip bytes
- automerge: 1143961
- loro: 1128590
- wa sqlite async: 778829
Explainer
Local-First Sync and CRDT Libraries: Domain Explainer#
The problem#
Two people edit the same shopping list. One is on a train with no signal, one is at home. Both change line three. When the train leaves the tunnel, the app has to decide what line three says.
A web app that writes every change to a server first never meets this question: the server sees edits one at a time and the last one wins, or the second one gets an error. A local-first app answers differently. Each device keeps its own full, editable copy, accepts edits with no network, and reconciles with other copies later. The term and its seven ideals come from Kleppmann et al., “Local-first software: you own your data, in spite of the cloud” (Onward! 2019, doi:10.1145/3359591.3359737, https://www.inkandswitch.com/local-first/). Offline storage alone does not give an app this property. A browser database can hold the data (the subject of survey 1.118) and still have no way to merge two divergent copies.
The hardware-store view#
Think of two aisles in the same store.
The CRDT aisle sells a rule for merging. A conflict-free replicated data type is a data structure designed so that any two copies that have seen the same set of edits end up identical, whatever order the edits arrived in. Shapiro et al. gave the formal treatment (“Conflict-free Replicated Data Types”, SSS 2011, doi:10.1007/978-3-642-24550-3_29). Yjs, Automerge, Loro and Diamond Types sit here. They hold the data and the merge rule, and they do not include a server. You supply any transport: a WebSocket relay, a file share, a peer-to-peer link.
The sync-engine aisle sells a pipeline between a database and the app. ElectricSQL, PowerSync, Zero, Triplit, Instant, Jazz, RxDB’s replication plugins and PouchDB replication sit here. They move rows between a server database and a local store on each device, and they decide what happens when two devices changed the same row. Most of them decide by making the server the referee: the server applies edits in order and tells every client what the result was.
Three ways to settle a conflict#
| Model | What decides | What the loser sees |
|---|---|---|
| CRDT merge | A mathematical rule built into the data type | Both edits survive where the type allows it (two inserts in a text), one wins where it cannot (two writes to the same field) |
| Server-authoritative | The server replays or rejects each client mutation in order | The client’s optimistic change is replaced by the server’s result |
| Last writer wins | A timestamp or version | The earlier write disappears |
No model is better in general. Text that two people type into at once needs a merge rule, because “last writer wins” would delete one person’s paragraph. A bank balance needs a referee, because a merge of two withdrawals can overdraw it. Rocicorp’s own documentation makes the point from the server-authoritative side: some conflicts in collaborative apps cannot be settled by any algorithm (https://zero.rocicorp.dev/docs/offline, read 2026-10-05).
What this category does not include#
- Storage in the browser (IndexedDB, OPFS, SQLite on WASM, service workers). That is survey 1.118.
- Live broadcast between connected clients (pub/sub, presence channels). That is survey 1.067.2. A collaboration app often uses both: a CRDT for the document and a channel for cursors.
- Hosted-only collaboration products. They are named as neighbors in S1 and not scored.
When you need this, and when you do not#
You probably need it if people edit the same data from more than one device, if edits must work with no connection for minutes or days, or if two people change one document at the same time.
You probably do not need it if the app is a viewer, if edits come from one device at a time, or if users are online nearly all the time and a failed write can simply be retried. In that case an ordinary API with optimistic updates is cheaper. Zero’s authors argue this explicitly and ship without offline writes for that reason (source: https://zero.rocicorp.dev/docs/offline, accessed 2026-10-05).
What it costs#
- Data size. CRDTs keep extra information so edits can be merged. Libraries differ in how much history they keep and whether it can be trimmed.
- Bundle size. Client libraries run from 13 kB to 411 kB gzipped before any app code, and the two Rust-based CRDT libraries add a WebAssembly file of about 1.1 MB gzipped each (measured 2026-10-05, see S2).
- Permissions. A CRDT document is the unit of sharing. Letting some users see only some rows or fields is a sync-engine feature, and the engines differ widely in how much they provide.
- Operations. Sync engines need a server to run. CRDT libraries need one too if peers are not directly connected, but a relay can know nothing about the data.
Where to go next#
The S1 files profile each option as a peer. S2 compares them on the same dimensions and reports a bundle-size measurement and a convergence probe. S3 gives four generic scenarios. S4 covers viability and hosted lock-in.
Sources#
- Kleppmann, Wiggins, van Hardenberg, McGranaghan (2019), Local-first software, Onward! 2019, doi:10.1145/3359591.3359737. The seven ideals and the framing of the problem.
- Shapiro, Preguica, Baquero, Zawirski (2011), Conflict-free Replicated Data Types, SSS 2011, doi:10.1007/978-3-642-24550-3_29. The formal definition of a CRDT.
- crdt.tech, the community CRDT reference (https://crdt.tech/), accessed 2026-10-05. An accessible secondary treatment.
- Zero documentation, Connection Status (https://zero.rocicorp.dev/docs/offline), accessed 2026-10-05.
S1: Rapid Discovery
S1 Approach: Local-First Sync and CRDT Libraries#
The category#
A local-first library lets each device edit its own copy of the data with no connection and merge with other copies on reconnect. Two families do that job, and they overlap less than their shared marketing suggests.
| Family | You get | You still supply |
|---|---|---|
| CRDT libraries (Yjs, Automerge, Loro, Diamond Types) | A data structure with a built-in merge rule, plus a way to encode edits as bytes | A transport, a server or relay if peers are not directly connected, storage, permissions |
| Sync engines over a database (ElectricSQL, PowerSync, Zero, Triplit, Jazz, InstantDB) | Replication between a server database and a local store, usually with a query or subscription model | A server process or hosted account, a schema on both sides, often the write path |
| Replication plugins (RxDB, PouchDB) | Replication protocol and conflict hooks over a local document database | A compatible backend (CouchDB, or an HTTP endpoint you write) |
Browser storage (1.118) is the layer beneath all three. Live broadcast (1.067.2) is the neighbor above: it carries cursors and presence while the libraries here carry the data.
Dimensions every profile uses#
- Conflict model. CRDT merge, server-authoritative, or last writer wins.
- What the server must be. A Postgres database, any backend you write, a CouchDB server, a relay, or a vendor’s cloud.
- Offline tolerance. Whether writes are accepted with no connection and for how long.
- Data shape. Documents (nested JSON, text) or relational rows with queries.
- Authorization. Whether permissions on synced data are built in or left to your server.
- History and undo.
- Bundle size. Measured in S2.
- License and the stall test. License from the registry and repository; stall test per the house method (open pull requests against pull requests merged since 2026-03-01, plus commits).
- Hosted lock-in. Whether a self-hosted path exists and what the license lets you do with it.
Scope decisions#
- Scored: open-source libraries with a self-hostable path. Thirteen are profiled.
- Neighbors, not scored: hosted-only products (see
neighbors.md). They can be a fine choice, but a survey cannot judge their durability the way it judges a repository. - RxDB and PouchDB: replication only. Their local storage is covered in 1.118.
- Replicache: profiled as a retired product, because it is still installed in production apps and its successor is in the same list.
How facts were gathered#
Versions, release dates and licenses come from the npm registry (https://registry.npmjs.org/, accessed
2026-10-05). Repository activity comes from the GitHub API on the same date. Behavior claims come from
each project’s own documentation, cited where used. A figure with no source is marked (unverified).
S1: CRDT Libraries#
Four libraries whose job is the merge rule. None includes a server. All encode edits as bytes that any transport can carry. Facts below come from the registry and GitHub on 2026-10-05.
Yjs#
- What it is: A CRDT framework for shared types (text, arrays, maps, XML). It is the most widely integrated of the four: its README lists editor bindings for many editors and a set of network providers (source: https://github.com/yjs/yjs, README read 2026-10-05).
- Conflict model: CRDT merge. Concurrent inserts in a text both survive; concurrent writes to the same map key resolve to one winner on every replica.
- Server: None required. Providers such as y-websocket (MIT, 3.1.0, 2026-08-06) relay updates; Hocuspocus (MIT, 4.7.0, 2026-09-09) is a maintained WebSocket backend with hooks for auth and persistence (source: https://registry.npmjs.org/, accessed 2026-10-05).
- Offline tolerance: No time limit in the data type. Persistence in the browser uses a provider (y-indexeddb), whose last release was 2023-11-02 (source: registry).
- Data shape: Documents. Text, lists, maps and XML trees. There is no query language.
- Auth: None in the library. A relay can authenticate connections; per-field permissions inside one document are not part of the model.
- History and undo: A selective
UndoManagerships in the package. Garbage collection is on by default and the docs say turning it off is how you restore old content (source: yjs README,gcsection). - License: MIT (npm). GitHub’s license detector reports NOASSERTION for the repository.
- Version and cadence: 13.6.33, released 2026-09-23; first published 2015 (source: registry).
- Stall test: 27 open pull requests, 11 merged since 2026-03-01, 130 commits (measured 2026-10-05). Healthy, with most work landing as direct commits.
Automerge#
- What it is: A JSON-document CRDT with a Rust core compiled to WebAssembly. A companion package, automerge-repo, adds networking, storage adapters and document handles.
- Conflict model: CRDT merge. When two peers set the same field, one value wins deterministically and the other stays readable as a recorded conflict (measured, see S2 merge probe: both writers’ values reported).
- Server: None required. automerge-repo ships relay and storage adapters; its
latestnpm tag pointed at 2.6.0-alpha.3 on 2026-10-05 (source: registry), so read its release notes before depending on it. - Offline tolerance: No time limit in the data type.
- Data shape: Nested JSON documents with text. Text edits go through a
splicefunction on a string field (read in the 3.5.0 type declarations). - Auth: None in the library.
- History and undo: Documents retain their change history, which the project presents as the basis of its version-control features (unverified: automerge.org was not read in this pass).
- License: MIT.
- Version and cadence: @automerge/automerge 3.5.0, released 2026-09-16 (registry).
- Stall test: automerge/automerge: 26 open, 108 merged, 193 commits. automerge-repo: 58 open, 124 merged, 98 commits (measured 2026-10-05). Healthy.
- Cost to note: The WebAssembly file is 3,644,225 bytes raw and 1,143,961 gzipped (measured 2026-10-05).
Loro#
- What it is: A CRDT library written in Rust with JavaScript (WASM) and Swift bindings. Its README lists a Fugue-based text type, a rich-text CRDT, a movable tree, a movable list and a last-write-wins map, plus time travel and shallow snapshots that work “like Git shallow clone” (source: https://github.com/loro-dev/loro README, read 2026-10-05).
- Conflict model: CRDT merge. Maps are last-write-wins per key.
- Server: None required.
- Offline tolerance: No time limit in the data type. Shallow snapshots exist to cap history growth.
- Data shape: Documents, with a tree type that matters for outliners and file-tree editors.
- Auth: None in the library.
- History and undo: Version control and checkout of past states are headline features; the repository also covers undo (README lists time travel; an undo manager was not read in this pass).
- License: MIT.
- Version and cadence: loro-crdt 1.16.4, released 2026-09-30; first published 2023-04-07 (registry).
- Stall test: 20 open, 159 merged, 183 commits (measured 2026-10-05). Healthy and the youngest of the three.
- Cost to note: WebAssembly file 3,398,779 bytes raw and 1,128,590 gzipped (measured 2026-10-05).
Diamond Types#
- What it is: A Rust CRDT for plain text, by Seph Gentle. Its README marks it WIP and says plain text is fully supported while JSON-style types are on a separate branch (source: https://github.com/josephg/diamond-types, read 2026-10-05).
- Conflict model: CRDT merge for text, with interoperability with operational transform.
- Server: None.
- Data shape: Text only in the released packages.
- License: ISC OR Apache-2.0 on npm (dual-licensed).
- Version and cadence: npm packages diamond-types-web and diamond-types-node are at 1.0.2, last published 2023-05-15; the README calls the Cargo package out of date (registry; README).
- Stall test: 6 open pull requests, 0 merged since 2026-03-01, and 20 commits (measured 2026-10-05). Zero merged pull requests with commits still landing fits a maintainer who commits directly, which the house rule says to compare against commits, not merges. The repository is live; the npm packages are three years old.
- Verdict: Live research code and a performance reference, not a drop-in dependency for an app that needs structured data or current npm releases.
What separates them#
- Bytes versus ecosystem. Yjs is small in JavaScript and has the widest set of editor bindings. Automerge and Loro carry a WebAssembly file of over a megabyte gzipped in exchange for a Rust core and richer history features.
- Data model. Yjs and Loro offer text, lists, maps and trees; Automerge is JSON-first.
- Maturity of the sync layer. Yjs has the most relays and providers; automerge-repo is the sync layer for Automerge; Loro’s sync is left to the app.
S1: Neighbors, Not Scored#
Hosted-only products#
These sync or collaboration products have no self-hostable open-source core of the kind this survey scores. They are named so a reader knows the category exists; nothing here was researched beyond the name, and no claim about price, license or behavior is made.
- Liveblocks (hosted collaboration infrastructure)
- Firebase Cloud Firestore (Google’s hosted document database with client offline persistence)
- Convex (hosted reactive backend)
- Ditto (commercial edge sync platform)
- Couchbase Mobile (commercial; sync gateway plus mobile database)
The lock-in test used elsewhere in this survey applies: can you run the server yourself, under a license that lets you? Where a product fails it, the choice is a vendor decision, and S4 covers how to keep it reversible.
Adjacent open-source projects, not scored#
- cr-sqlite (vlcn-io): CRDT tables inside SQLite. MIT. 8 open pull requests, 5 merged and 5 commits since 2026-03-01; npm package last released 2024-01-17 (measured 2026-10-05; registry). Low activity; a research-grade option rather than a dependable one.
- PGlite (electric-sql): Postgres in the browser, covered in 1.118; it is the local half of Electric’s database-sync pattern.
- Dexie Cloud: the paid sync service for Dexie, covered in 1.118 as a hosted option.
Neighboring surveys#
- 1.118 Browser Offline Storage covers where the data lives on the device, quota and eviction. Every client in this survey needs it.
- 1.067.2 Real-Time Broadcast and Pub/Sub covers live channels. A document editor typically uses a CRDT for the content and a channel layer for cursors and presence.
- 1.187 Embedded Databases and 1.188 Version-Controlled Branchable Databases cover server-side and embedded databases, including branch-and-merge storage, which is a different answer to the same divergence problem.
- 1.111 State Management Libraries covers in-memory state; some local-first clients ship their own store.
- 1.113.1 Data Grids covers the table UI a team dashboard would put over synced rows.
S1 Recommendation: Choose by Conflict Model, Then by Server#
The map#
| If the data is | And the server is | Start with |
|---|---|---|
| Shared text or a nested document edited by several people at once | Anything, or a thin relay | Yjs (widest editor bindings), Loro (trees, version control), Automerge (JSON history) |
| Rows in Postgres, edited offline by one owner at a time | Postgres you run | PowerSync (offline writes queue to your backend) |
| Rows in Postgres, read live, written through an API | Postgres you run | ElectricSQL |
| Rows in Postgres, mostly online, instant UI | Postgres 15+ with logical replication | Zero |
| Rows in MongoDB, MySQL or SQL Server | The same | PowerSync |
| Documents replicating to CouchDB | CouchDB | PouchDB (or RxDB’s CouchDB plugin) |
| Documents replicating to your own HTTP or GraphQL endpoint | A backend you write | RxDB replication with a conflict handler |
| A database and sync in one package, self-hosted | The vendor’s server | InstantDB (Apache-2.0); Jazz if you accept the 2.0 transition |
What S1 rules out#
- Zero for offline-edit workloads. Its documentation says writes fail while disconnected.
- Triplit for new long-lived work. Stalled since early 2026 under AGPL.
- Replicache for new work. Maintenance mode; Zero is the named successor.
- Diamond Types as an app dependency. Text only and its npm packages date from 2023.
Still undecided at this level#
Bundle cost and behavior under concurrent offline edits separate the survivors; both are measured in the S2 files.
S1: RxDB and PouchDB Replication#
Only the replication layer is compared here. Their local storage engines and quota behavior are in survey 1.118.
RxDB replication#
- What it is: A replication protocol built into the RxDB document database, with one plugin per transport. The docs list HTTP, GraphQL, WebSocket, CouchDB, WebRTC peer-to-peer, Firestore, MongoDB, Supabase, Google Drive, Microsoft OneDrive, NATS, Appwrite and RxServer (source: https://rxdb.info/replication.html, accessed 2026-10-05).
- Conflict model: Master-wins by default: “The default conflict handler will always drop the fork state
and use the master state.” A custom
conflictHandlerper collection can implement a different rule (same page). This is server-authoritative with a hook, so a CRDT can be plugged in where the app wants merging. - Server: Any backend that can implement the pull and push endpoints, or one of the listed services. You write that endpoint for the HTTP and GraphQL cases.
- Offline tolerance: Writes continue locally and replication resumes when the client is online. No duration limit is documented on the page read.
- Data shape: JSON documents with schemas.
- Auth: Not part of replication; handled by your endpoint.
- License and cost: Core is Apache-2.0. The replication page did not say which plugins are premium. The 1.118 survey established that RxDB’s IndexedDB and OPFS storages are premium (source: https://rxdb.info/rx-storage.html), so confirm each replication plugin’s tier on the RxDB premium page before planning around it.
- Version and cadence: rxdb 17.6.0, released 2026-10-05 (registry).
- Stall test: 9 open, 1,116 merged, 1,276 commits (measured 2026-10-05). Very active.
- Bytes: Core is 41,733 bytes gzipped and core plus the replication plugin is 54,953 (measured 2026-10-05).
PouchDB replication#
- What it is: Replication between PouchDB and any server that speaks the CouchDB protocol: Apache CouchDB, and compatible servers. Replication is multi-master: “you don’t have to care which one is the ‘master’ and which one is the ‘follower’” (source: https://pouchdb.com/guides/replication.html, accessed 2026-10-05).
- Conflict model: A revision tree, like Git history. When two revisions conflict, the database picks a deterministic winner that every replica agrees on, and the losing revision stays in the tree so an app can surface it or ignore it (same page). This is last-writer-wins by revision with the loser retained.
- Server: CouchDB protocol. If you do not already have one, that is a new database to operate.
- Offline tolerance: Live replication with a
retryoption handles dropped connections (same page). No time limit in the model. - Data shape: JSON documents. Per-user or per-tenant filtering is possible but has a known cost: a filtered replication reads through the changes feed (general CouchDB behavior; not verified in this pass).
- Auth: CouchDB’s own users and database-level permissions; there is no row-level rule system.
- License: Apache-2.0.
- Version and cadence: pouchdb 9.0.0, released 2024-06-21 (registry). The repository is still worked on.
- Stall test: 59 open, 46 merged, 96 commits since 2026-03-01 (measured 2026-10-05). Alive but slow: fewer than one merged pull request a week, and the last npm release is more than two years old.
- Bytes: pouchdb-browser is 43,095 bytes gzipped (measured 2026-10-05).
When each is the answer#
- Replicating to a CouchDB: PouchDB is the original and the least translation; RxDB’s CouchDB plugin is the alternative if you want RxDB’s reactive queries.
- Replicating to a backend you already wrote: RxDB’s HTTP or GraphQL plugin, with a conflict handler.
- Replicating to Postgres: PowerSync, Electric or Zero are built for it. RxDB’s Supabase and MongoDB plugins cover those two services specifically.
S1: Sync Engines That Bring Their Own Database#
Three products that are a database and a sync layer in one package. You adopt their server, their query model and their permission system together.
Triplit#
- What it is: An open-source “full stack database” that syncs queries between a server and browser clients, with a client-side cache and pluggable storage (source: https://github.com/aspen-cloud/triplit README, read 2026-10-05).
- Conflict model: Property-level conflict resolution with rollback and retry on failed updates (README).
- Server: Triplit’s own server, with durable storage and an admin dashboard.
- Offline tolerance: README claims an offline mode with automatic reconnection (vendor).
- Data shape: Relational queries over schema-typed collections.
- Auth: “Authorization that’s enforced on the server for both read and writes” (README).
- License: AGPL-3.0 (npm and GitHub). The AGPL’s network clause is the main adoption question for anyone who would modify and host it.
- Version and cadence: @triplit/client 1.0.50, released 2025-07-31 (registry). Supabase announced that Triplit’s co-founder joined it, with an intent to further open-source the code and document the patterns used, and said it was not integrating Triplit into its platform (source: https://supabase.com/blog/triplit-joins-supabase, accessed 2026-10-05).
- Stall test: 4 open pull requests, 0 merged since 2026-03-01, and 0 commits in that window; last push 2026-01-19 (measured 2026-10-05). Inbound patches with no merges and no commits is the stall pattern. The cause has a date: Supabase hired its co-founder on 2025-10-08 (supabase.com/blog/triplit-joins-supabase).
- Verdict: Stalled. Fork material under AGPL; do not start a long-lived product on it.
Jazz#
- What it is: A local-first database from Garden Computing. Two different products share the name.
The npm
latesttag (jazz-tools 0.20.19, 2026-07-03, MIT) is “classic” Jazz, built on collaborative values (CoValues) in a CRDT format that keep full edit history. Thealphatag (2.0.0-alpha.59) is a rewrite with a relational API, row-level permissions and “no separate API layer,” built on a Rust core (sources: registry; https://jazz.tools/docs and https://github.com/garden-co/jazz, accessed 2026-10-05). - Conflict model: Classic Jazz uses CRDTs (search result summarizing the project’s own materials; the classic docs were not read in this pass). The conflict model of the 2.0 rewrite was not stated on the pages read (unverified).
- Server: Jazz Cloud (managed) or self-hosted sync infrastructure (README).
- Auth: Group-based in classic; row-level in 2.0.
- Offline tolerance: Stated as “instant, works offline” in the 2.0 overview (vendor).
- License: MIT.
- Stall test: 92 open, 2,257 merged, 13,781 commits since 2026-03-01 (measured 2026-10-05). The commit count reflects a rewrite in progress, not steady maintenance of the classic line.
- Verdict: Very active and in transition. The version people can depend on (0.20) and the version being developed (2.0 alpha) have different APIs.
InstantDB#
- What it is: A real-time database with a client SDK, a query language and a permission system. Its README says user data is stored “as triples in one big Postgres database,” that permissions run through Google’s CEL, and that the SDK persists a query cache to IndexedDB or AsyncStorage (source: https://github.com/instantdb/instant, accessed 2026-10-05).
- Conflict model: “Optimistic updates and rollbacks” with a triple store; the documented merge rule for two writes to one attribute was not found (unverified, likely last write per attribute).
- Server: Instant’s hosted service, or self-hosted. The repository has a self-hosting directory, and the docs navigation lists VPS and AWS guides, which were not read.
- Offline tolerance: The query cache survives reloads. Whether writes queue durably while offline was not confirmed from the documentation read (unverified).
- Data shape: Relational-style links between namespaces, stored as triples.
- License: Apache-2.0.
- Version and cadence: @instantdb/core 1.0.67, released 2026-08-31 (registry).
- Stall test: 12 open, 495 merged, 415 commits (measured 2026-10-05). Active.
- Verdict: Self-hostable under Apache-2.0, so it counts as scored. Read its offline-write behavior before choosing it for flaky-network work.
S1: Sync Engines Over an Existing Database#
Four engines that replicate rows between a server database and a client. Three of them (ElectricSQL, PowerSync, Zero) are built around a Postgres you already run; PowerSync also reads other databases. The fourth, Replicache, is the retired predecessor of Zero.
ElectricSQL#
- What it is: A read-path sync engine. Its docs say “Electric does read-path sync. It syncs data out-of Postgres, into local apps and services. Electric does not do write-path sync” (source: https://electric.ax/docs/guides/writes, accessed 2026-10-05). The documentation host moved from electric-sql.com to electric.ax; the old address answered with a 301 on that date.
- Conflict model: Not prescribed. Writes go through your own API or into a local database that syncs. The docs say conflicts “are extremely rare and can be mitigated well by strategies like presence” (same page), which is a position, not a mechanism.
- Server: Postgres is required. Clients subscribe to “shapes” (a table with a filter and column set) over plain HTTP.
- Offline tolerance: The docs list four write patterns; the last three (optimistic state, shared persistent optimistic state, through-the-database sync) support offline writes. How long a client can be away before it must refetch a shape was not found in this pass (unverified).
- Data shape: Relational rows. Pairs with PGlite (Postgres in the browser) for local queries.
- Auth: Electric enforces nothing itself. Authorization is an HTTP layer you build, as a proxy or as a token-issuing “gatekeeper” (source: https://electric.ax/docs/guides/auth, accessed 2026-10-05).
- History and undo: None; it is not a history store.
- License: Apache-2.0 for the client (npm) and the repository.
- Version and cadence: @electric-sql/client 1.5.28, released 2026-09-09 (registry).
- Stall test: 96 open, 584 merged, 574 commits (measured 2026-10-05). Very active.
PowerSync#
- What it is: A service that replicates a backend database into SQLite on the client, with SDKs for JavaScript, React Native, Flutter, Kotlin, Swift and .NET (source: https://docs.powersync.com/intro/powersync-overview, accessed 2026-10-05).
- Conflict model: Server-authoritative. Client writes go into an upload queue of PUT, PATCH and DELETE operations that your backend applies; the docs describe “a server-authoritative architecture with a checkpoint system” (source: https://docs.powersync.com/handling-writes/writing-client-changes, accessed 2026-10-05). What your backend does with two edits to one row is your code.
- Server: The source database can be Postgres, MongoDB, MySQL, SQL Server, Azure DocumentDB or Convex (overview page). You also write the endpoint that receives uploaded writes. The PowerSync service runs from a public Docker image.
- Offline tolerance: Writes queue locally in SQLite and upload on reconnect. A documented limit on queue length or offline duration was not found (unverified).
- Data shape: Relational on the client (SQLite); the source can be document or relational.
- Auth: Data is partitioned by rules tied to user identity, so a client only receives the rows its rules allow (overview page: “Data partitioning based on user permissions”).
- License: Client SDK Apache-2.0 (registry). The service repository’s LICENSE file is the Functional Source License 1.1 with an Apache-2.0 future license (FSL-1.1-ALv2), read 2026-10-05. FSL permits most uses except offering a competing service, and each release converts to Apache-2.0 after two years (term from the FSL text; not re-read in this pass).
- Version and cadence: @powersync/web 2.4.2, released 2026-10-01 (registry).
- Stall test: powersync-js 22 open, 188 merged, 248 commits; powersync-service 22 open, 222 merged, 221 commits (measured 2026-10-05). Very active.
- Hosted: A managed offering exists. Its pricing was not examined here.
Zero#
- What it is: A sync engine from Rocicorp that keeps a local normalized store and runs queries against it first, then syncs continuously. Reads and writes “hit that local store first, then sync continuously with your server in the background” (source: https://zero.rocicorp.dev/docs/introduction, accessed 2026-10-05).
- Conflict model: Server-authoritative. A mutator runs optimistically on the client and again on the server; the server’s result replaces the optimistic one (source: https://zero.rocicorp.dev/docs/mutators).
- Offline tolerance: Reads work while disconnected. Writes are rejected with an offline error in the disconnected, error and needs-auth states; they queue only during the short “connecting” state. The docs say the team does not support offline writes because “for most of the apps we want to support, the user is online the vast majority of the time and the cost to support offline is extremely high,” and that revisiting it is “not a priority right now” (source: https://zero.rocicorp.dev/docs/offline, accessed 2026-10-05).
- Server: Postgres 15 or later with
wal_levelset to logical. Full support listed for AWS RDS and Aurora, PlanetScale for Postgres, Neon and Google Cloud SQL; Supabase, Fly.io and Render work with limits around event triggers (source: https://zero.rocicorp.dev/docs/connecting-to-postgres, accessed 2026-10-05). - Data shape: Relational, with a query language and relationships.
- Auth: Enforced through context passed to query definitions and mutators.
- License: Apache-2.0; the docs say the team has “no plans to ever change the licensing of the core product” and sells Zero as a service and paid support (source: https://zero.rocicorp.dev/docs/open-source).
- Version and cadence: @rocicorp/zero 1.9.0, released 2026-08-14 (registry).
- Stall test: rocicorp/mono (which also holds Replicache): 2 open, 868 merged, 830 commits (measured 2026-10-05).
- Fit: An online-first app that wants instant UI and shared live data. Not a fit when offline edits matter.
Replicache#
- What it is: Rocicorp’s earlier sync library. Its site says it has moved to maintenance mode, that no new features will be added, and that Zero is the recommended successor (source: https://replicache.dev, accessed 2026-10-05).
- License: The site says it is open source and no longer charges. The npm package’s license field points to Rocicorp’s terms page rather than an SPDX identifier (registry: 15.3.0, 2025-07-02), so read those terms before depending on it.
- Stall test: The old rocicorp/replicache repository is archived (last push 2022-05-07); the code lives in rocicorp/mono.
- Verdict: Declared maintenance mode, not a stall. Use for existing installations; start new work elsewhere. Its model (client queues mutations, server replays them) is the ancestor of Zero’s mutators.
S2: Comprehensive
S2 Approach: Measurements and Side-by-Side Comparison#
S2 compares thirteen libraries on the same dimensions and reports three measurements, each reproducible from
harness/1-118-1-local-first-sync-crdt/.
| File | Content |
|---|---|
feature-comparison.md | Conflict model, server, offline writes, data shape, authorization, history, license, lock-in |
bundle-size.md | esbuild bytes for 12 client entry points, plus WebAssembly file sizes |
merge-probe.md | Concurrent offline edits merged in Yjs, Automerge and Loro, checked for convergence |
durability-signals.md | Stall test and release dates for 17 repositories |
measurement-plan.md | Which levels were run, which were deferred and why |
recommendation.md | What the comparison supports |
Environment for every measurement: Node v20.20.0, esbuild 0.25.0, Linux 6.6 arm64 under WSL2, run 2026-10-05.
Library versions are pinned in the harness package.json.
S2: Bundle Size per Client Library#
Method#
Each client’s smallest realistic entry point was bundled with esbuild 0.25.0 (--bundle --minify,
ESM, browser platform, target es2022) and compressed with gzip level 9 and brotli quality 11. WebAssembly
files were kept external and sized separately. Node v20.20.0, Linux arm64 (WSL2), run 2026-10-05.
Script: harness/1-118-1-local-first-sync-crdt/bundle_size.mjs; raw output: results_bundle.json.
Results#
(measured 2026-10-05; esbuild 0.25.0; versions pinned in the harness package.json)
| Entry point | Minified bytes | Gzip | Brotli |
|---|---|---|---|
| loro-crdt 1.16.4 (default entry, JS glue; Automerge above is its slim entry) | 89,058 | 12,948 | 11,150 |
| @automerge/automerge 3.5.0 (slim entry, JS glue) | 63,260 | 15,273 | 13,291 |
| @electric-sql/client 1.5.28 (ShapeStream, Shape) | 57,118 | 18,019 | 15,985 |
| yjs 13.6.33 (Doc, applyUpdate, encodeStateAsUpdate) | 78,386 | 23,735 | 21,360 |
| rxdb 17.6.0 core | 129,294 | 41,733 | 36,944 |
| pouchdb-browser 9.0.0 | 126,847 | 43,095 | 38,032 |
| @instantdb/core 1.0.67 (init) | 143,772 | 44,375 | 39,238 |
| rxdb core plus replication plugin | 175,331 | 54,953 | 48,566 |
| @triplit/client 1.0.50 (TriplitClient) | 239,006 | 72,175 | 63,037 |
| @rocicorp/zero 1.9.0 (client entry) | 305,512 | 95,795 | 83,167 |
| @powersync/web 2.4.2 (PowerSyncDatabase) | 535,867 | 159,410 | 101,621 |
| jazz-tools 0.20.19 (core entry, classic) | 1,194,610 | 410,827 | 340,630 |
WebAssembly files, separate downloads (measured 2026-10-05):
| File | Raw bytes | Gzip |
|---|---|---|
| automerge.wasm | 3,644,225 | 1,143,961 |
| loro_wasm_bg.wasm (browser build) | 3,398,779 | 1,128,590 |
| wa-sqlite-async.wasm (shipped with @powersync/web’s dependency) | 2,256,849 | 778,829 |
How to read the numbers#
- The two Rust CRDT libraries are small in JavaScript and large in WebAssembly. Their JavaScript is 13 to 15 kB gzipped, and each brings a WebAssembly file of about 1.1 MB gzipped. Yjs is pure JavaScript at 23.7 kB with no second file. For a page that loads a document editor, the total is about 24 kB (Yjs) against about 1.14 MB (Loro) or 1.16 MB (Automerge).
- Engines with a local database cost more. PowerSync’s client brings a SQLite build; the wa-sqlite file listed is one of several variants in the package, and which variant a given app loads depends on the browser and configuration, so treat the 779 kB figure as one example, not the total.
- Electric’s client is small because the store is separate. It streams shapes and leaves local storage to the app or to PGlite, whose size is not included.
- Jazz is the heaviest entry measured, and this is the classic 0.20 line. The 2.0 alpha, built on a Rust core, was not measured.
- Zero’s client includes its normalized store and query engine.
Limits of the measurement#
- Bundle bytes are not runtime speed or memory. No library was executed for timing.
- Entry points differ in scope: Yjs’s number covers a document type only; Zero’s covers a store and a query engine. They are the cost of the smallest import that does that library’s job, not like-for-like.
- Tree-shaking by a real app’s bundler will vary; Loro’s browser entry resolved through esbuild’s default conditions and others may pick a different entry.
- The numbers were taken on aarch64; compression sizes do not depend on architecture, but the claim is limited to what was run.
S2: Durability Signals and the Stall Test#
Method#
For each repository: open pull requests, pull requests merged since 2026-03-01, and commits since that date,
from the GitHub API (script stall_test.py, results in results_stall.json, run 2026-10-05). Latest release
date and license from the npm registry. A stall is inbound patches with none merged; a project that commits
directly is compared on commits, not merges (house method, ADDING-RESEARCH.md).
Results#
(measured 2026-10-05; window 2026-03-01 to 2026-10-05; GitHub API)
| Repository | Open PRs | Merged since | Commits since | Last push | Latest npm release |
|---|---|---|---|---|---|
| yjs/yjs | 27 | 11 | 130 | 2026-10-03 | yjs 13.6.33, 2026-09-23 |
| yjs/y-websocket | 11 | 1 | 15 | 2026-08-06 | 3.1.0, 2026-08-06 |
| automerge/automerge | 26 | 108 | 193 | 2026-10-05 | 3.5.0, 2026-09-16 |
| automerge/automerge-repo | 58 | 124 | 98 | 2026-10-05 | 2.6.0-alpha.3 (latest tag), 2026-08-07 |
| loro-dev/loro | 20 | 159 | 183 | 2026-09-30 | 1.16.4, 2026-09-30 |
| josephg/diamond-types | 6 | 0 | 20 | 2026-09-02 | 1.0.2, 2023-05-15 |
| electric-sql/electric | 96 | 584 | 574 | 2026-10-03 | 1.5.28, 2026-09-09 |
| powersync-ja/powersync-js | 22 | 188 | 248 | 2026-10-05 | 2.4.2, 2026-10-01 |
| powersync-ja/powersync-service | 22 | 222 | 221 | 2026-10-05 | not an npm client |
| rocicorp/mono (Zero, Replicache) | 2 | 868 | 830 | 2026-10-05 | zero 1.9.0, 2026-08-14 |
| rocicorp/replicache (archived) | 0 | 0 | 0 | 2022-05-07 | replicache 15.3.0, 2025-07-02 |
| aspen-cloud/triplit | 4 | 0 | 0 | 2026-01-19 | 1.0.50, 2025-07-31 |
| garden-co/jazz | 92 | 2,257 | 13,781 | 2026-10-05 | 0.20.19 latest; 2.0.0-alpha.59 |
| instantdb/instant | 12 | 495 | 415 | 2026-09-28 | 1.0.67, 2026-08-31 |
| pubkey/rxdb | 9 | 1,116 | 1,276 | 2026-10-05 | 17.6.0, 2026-10-05 |
| apache/pouchdb | 59 | 46 | 96 | 2026-10-02 | 9.0.0, 2024-06-21 |
| vlcn-io/cr-sqlite (neighbor) | 8 | 5 | 5 | 2026-08-10 | 0.16.3, 2024-01-17 |
Readings#
- Triplit is stalled. Four inbound pull requests, none merged, no commits since March, last push January. Cause with a date: its co-founder joined Supabase (post dated 2025-10-08, https://supabase.com/blog/triplit-joins-supabase), and the post says the code would be further open-sourced, not integrated). The code is AGPL-3.0 and usable as a fork base.
- Diamond Types commits without merging pull requests. Twenty commits in the window and no merges is a direct-commit maintainer, and the repository is live. The npm packages are 2023 builds, so the published artifact lags the source.
- PouchDB is alive but slow. About one merged pull request a week against 59 open, and no npm release since June 2024.
- y-websocket merges rarely (1 of 12 inbound) but commits (15). A thin provider with a stable surface.
- Jazz’s commit count is a rewrite. 13,781 commits since March reflect the 2.0 alpha, not steady maintenance of 0.20.
- Replicache is retired by announcement, not stalled: the archived repository predates the move into the monorepo, and active work continues on Zero.
- cr-sqlite has five commits and five merges in seven months and no release since 2024-01: low activity, not zero.
- automerge-repo’s npm
latesttag is an alpha. Version pinning is advisable.
Caveats#
- Commit and merge counts measure activity, not quality. A busy repository can break users often.
- Monorepos mix packages: rocicorp/mono counts Zero and Replicache work together, and garden-co/jazz mixes Rust and TypeScript work.
- GitHub search counts were taken once; they move daily.
S2: Feature Comparison#
Every cell cites its source on first use in S1; a dash means the library does not provide the feature, and “not found” means the documentation read did not state it. Registry and repository facts were read on 2026-10-05.
Conflict model, server and offline tolerance#
| Library | Conflict model | What the server must be | Offline writes |
|---|---|---|---|
| Yjs | CRDT merge | Nothing, or a relay (y-websocket, Hocuspocus) | Yes, no time limit in the type |
| Automerge | CRDT merge, conflicts remain readable | Nothing, or a relay (automerge-repo adapters) | Yes, no time limit in the type |
| Loro | CRDT merge, LWW map | Nothing | Yes, no time limit in the type |
| Diamond Types | CRDT merge (text) | Nothing | Yes |
| ElectricSQL | Not prescribed; you choose the write path | Postgres, plus your API for writes | Depends on write pattern; patterns 2 to 4 yes |
| PowerSync | Server-authoritative; your backend applies queued writes | Postgres, MongoDB, MySQL, SQL Server and others, plus an upload endpoint | Yes, queued in client SQLite |
| Zero | Server-authoritative mutators | Postgres 15+ with logical replication | No: writes rejected while disconnected |
| Replicache | Server replays queued mutations | Your backend implements push and pull | Yes (maintenance mode) |
| Triplit | Property-level resolution | Triplit server | README claims yes (stalled) |
| Jazz | CRDT in classic; not stated for 2.0 | Jazz Cloud or self-hosted sync | Yes, per vendor |
| InstantDB | Optimistic with rollback; merge rule not found | Instant server (Postgres underneath) | Reads cached; writes not confirmed |
| RxDB replication | Master-wins by default, custom handler | Your endpoint, or one of 13 listed services | Yes |
| PouchDB replication | Revision tree, deterministic winner, loser kept | CouchDB protocol server | Yes, with retry |
Data shape, auth, history#
| Library | Data shape | Authorization on synced data | History or undo |
|---|---|---|---|
| Yjs | Text, arrays, maps, XML | Not in the library; relay can authenticate connections | UndoManager; gc on by default |
| Automerge | Nested JSON with text | Not in the library | Keeps change history (unverified, docs not read) |
| Loro | Text, rich text, tree, list, map | Not in the library | Time travel, shallow snapshots |
| Diamond Types | Text | Not in the library | Not found |
| ElectricSQL | Relational rows | Yours, via proxy or gatekeeper over HTTP | None |
| PowerSync | Relational (client SQLite) | Per-user data partitioning rules | None |
| Zero | Relational with relationships | Context passed to queries and mutators | None |
| Replicache | Key-value | Your backend | None |
| Triplit | Typed collections, relational queries | Server-enforced read and write rules | Not found |
| Jazz | CoValues (classic); relational (2.0) | Groups (classic); row-level (2.0) | Full edit history in classic |
| InstantDB | Linked namespaces over triples | CEL-based permission rules | Not found |
| RxDB replication | JSON documents | Your endpoint | Not found |
| PouchDB replication | JSON documents | CouchDB database-level | Revision tree keeps losers |
License and lock-in#
| Library | License | Self-hostable | Hosted tie-in |
|---|---|---|---|
| Yjs | MIT | Yes | None required |
| Automerge | MIT | Yes | None required |
| Loro | MIT | Yes | None required |
| Diamond Types | ISC (repo Cargo.toml); npm lists ISC OR Apache-2.0 | Yes | None |
| ElectricSQL | Apache-2.0 | Yes | Docs host moved to electric.ax; hosted tier not examined |
| PowerSync | Client Apache-2.0; service FSL-1.1-ALv2 | Yes (Docker image) | Managed service exists; FSL bars competing hosted offerings |
| Zero | Apache-2.0 | Yes | Rocicorp sells Zero as a service |
| Replicache | Custom terms page; code public | Yes | None |
| Triplit | AGPL-3.0 | Yes | Hosted cloud status not examined |
| Jazz | MIT | Yes, per README | Jazz Cloud is the default |
| InstantDB | Apache-2.0 | Yes, repository has self-hosting directory | Hosted service is the default |
| RxDB | Apache-2.0 core; premium plugins | Yes | Premium tier for some plugins |
| PouchDB | Apache-2.0 | Yes | None |
What the tables show#
- Only the CRDT libraries and the replication plugins have no database prerequisite. Every other engine assumes a specific server.
- Authorization is a gap in the CRDT family and a feature in the engines. The engines differ in whether you write it (Electric), declare it (PowerSync rules, Instant CEL, Zero context) or inherit it (CouchDB).
- Offline-write support is not implied by the category. Zero rejects writes while disconnected; Instant’s behavior is unconfirmed; Electric leaves it to the pattern.
- History is a CRDT-family strength. Loro and classic Jazz retain history by design; Yjs trades it for garbage collection by default.
S2: Measurement Plan#
Levels for this subject, cheapest first. Default cut line: one container, no external service, no second machine.
| Level | What it settles | Cost | Rung reachable | Coverage | Status |
|---|---|---|---|---|---|
| L0 Registry and GitHub facts | Versions, licenses, stall test | Minutes | measured-local | All 17 repositories | Done (durability-signals.md) |
| L1 Bundle size with esbuild | Cost of each client in bytes | Minutes | measured-local | 12 client entries; Diamond Types and Replicache not bundled | Done (bundle-size.md) |
| L2 Convergence probe, in process | Whether concurrent offline edits converge and lose no inserts | An hour | measured-local | Yjs, Automerge, Loro | Done (merge-probe.md) |
| L3 Sync round trip against a local Postgres | Time to first sync, reconnect after hours offline, write queue behavior | Days; needs a database and a server per engine | measured-local | Electric, PowerSync, Zero, RxDB | Deferred |
| L4 Offline duration limits | How long a client can be away before a full resync (Electric shapes, PowerSync queue, CouchDB checkpoints) | Days | measured-local | Engines | Deferred |
| L5 Document growth over long histories | Size of the encoded document after N edits, and load time | A day | measured-local | Yjs, Automerge, Loro | Deferred |
| L6 Browser measurement (floor model) | Reader reruns the convergence probe in a tab | A day | measured-browser | Yjs, Loro, Automerge (WASM loads in browsers) | Candidate |
Why the cut line sits at L2#
L3 and L4 need a database server and a server process per engine, which breaks the one-container rule. They would answer the questions most readers care about most (how long can a device be away, what happens at reconnect), and they are the main gap in this survey. L5 fits one container and was deferred for time; it is the measurement that would separate Loro and Automerge from Yjs on storage growth.
Coverage gaps#
- The engines cannot be measured at L2. Every engine needs a server, so the probe covers the CRDT libraries only. The conflict rules of the engines come from their documentation, not from runs.
- Zero’s offline behavior comes from documentation, which is unambiguous, but was not run.
S2: Merge Convergence Probe#
What was tested#
Replicas of one document were edited concurrently with no connection between them, then merged. Three
libraries were run: Yjs 13.6.33, Automerge 3.5.0 and Loro 1.16.4. Diamond Types was not run because its
npm packages date from 2023 and cover text only. The engines (Electric, PowerSync, Zero and the rest)
were not run: they need a server, which is outside the one-container cut line (see measurement-plan.md).
Script: harness/1-118-1-local-first-sync-crdt/merge_probe.mjs; output: results_merge.json.
Environment: Node v20.20.0, Linux arm64 (WSL2), 2026-10-05.
Checks#
- Fixed scenario. Two replicas start from “Hello world”. One inserts “big " at position 6, the other inserts “new " at the same position and appends “!”. Merge in both directions.
- Same-key conflict. Both replicas set one map key to different values, then merge.
- Seeded random edits. 200 seeded trials per library and mode. Three replicas each make 12 random edits, then merge in three different orders and in a full mesh with a second round. Two modes: inserts only (every inserted character is unique, so a missing one is a lost insert), and inserts plus deletes (convergence only, since a delete can legitimately remove an insert).
A trial passes when every merge order and every mesh replica ends with identical text.
Results#
(measured 2026-10-05; yjs 13.6.33, @automerge/automerge 3.5.0, loro-crdt 1.16.4; Node v20.20.0; 200 trials, 3 replicas, 12 edits per replica, seeds 1 to 200)
| Library | Fixed scenario (both inserts kept, both orders agree) | Divergent trials, inserts only | Lost inserts, inserts only | Divergent trials, inserts and deletes |
|---|---|---|---|---|
| Yjs | Pass: “Hello big new world!” | 0 of 200 | 0 | 0 of 200 |
| Automerge | Pass: “Hello big new world!” | 0 of 200 | 0 | 0 of 200 |
| Loro | Pass: “Hello big new world!” | 0 of 200 | 0 | 0 of 200 |
Same-key conflict: all three converged on one value, the one written by the second replica (“from-2”). Yjs and Loro were given fixed peer identifiers; Automerge assigns random actor identifiers, so the winner there can differ between runs, and the tie-break rule of each library was not tested beyond this one case. Automerge also reported both competing values through its conflicts accessor (two values listed).
What this does and does not show#
- It shows that the three libraries merge simple concurrent text and map edits without divergence or lost inserts on a small seeded workload. That is the baseline claim of a CRDT, and the probe confirmed it holds in this setup.
- It does not show that merged results are what a user wanted. Convergence means all replicas agree, not that the text reads well. Interleaving of long concurrent insertions at one position is a known quality difference between text algorithms; this probe used single characters and did not measure it.
- It does not test long histories, binary formats across versions, rich text marks, trees, or network behavior.
- Timings were recorded in the output file but are not reported here: the adapters copy and re-import documents in different ways per library, so the times are not comparable.
Why one probe covers three libraries#
The three expose different APIs for the same operations (Yjs and Loro apply byte updates; Automerge merges document objects). The probe wraps each behind identical insert, remove, fork and absorb operations, so the check is the same for each.
S2 Recommendation#
What the comparison supports#
- The three CRDT libraries converged on every probe run. On 200 seeded trials each, with three replicas and 12 edits per replica, Yjs, Automerge and Loro produced identical text across all merge orders with no lost inserts (measured 2026-10-05). Choose among them on bytes, data model and ecosystem.
- Bytes separate Yjs from the Rust libraries. Yjs is 23,735 bytes gzipped as one JavaScript file; Loro and Automerge are 12,948 and 15,273 bytes of JavaScript plus a WebAssembly file near 1.1 MB gzipped (measured 2026-10-05).
- Among engines with a documented write path, Zero is the only one that rejects writes while disconnected. The others queue them or leave the choice to the write pattern (Electric’s client has no write path; InstantDB’s offline write queue is unverified). If offline edits matter, it is out.
- Authorization differs more than conflict handling does. CRDT libraries have none; Electric leaves it to your proxy; PowerSync, Instant and Zero declare it; CouchDB has database-level rules.
- Two projects fail the durability test. Triplit is stalled; Replicache is retired by announcement. PouchDB is alive at a slow pace, and Diamond Types is live source with 2023 packages.
- Hosted lock-in depends on license, not on the open-source label. PowerSync’s service is FSL-1.1-ALv2, Triplit is AGPL-3.0, Replicache has custom terms, and the rest of the scored set is MIT, Apache-2.0 or ISC.
What it does not support#
- Any claim about sync latency, reconnect time after long offline periods, or document growth. Those levels were not run.
- Any claim that one engine merges better than another. Only the CRDT libraries were run.
S3: Need-Driven
S3: Four Situations#
| Persona | Decides the choice | Eliminates | Leading options |
|---|---|---|---|
| Collaborative document editor | Concurrent edits to the same text; cursors; undo | Zero, Electric alone, PouchDB | Yjs, Loro, Automerge |
| Field-service app on flaky networks | Days offline; server owns the records; photos and forms | Zero, Triplit, Replicache for new work | PowerSync, RxDB replication, PouchDB with CouchDB |
| Multi-device notes or to-do app | One owner, several devices; little server appetite | Engines that need a database you run | CRDT documents with a relay; InstantDB or Jazz if hosted is acceptable |
| Team dashboard with offline edits | Shared relational rows; permissions by team; reconcile with a system of record | CRDT-only stacks, Zero | PowerSync, ElectricSQL with a write path, RxDB replication |
Each persona file lists what it eliminates, what remains, and the deciding criteria. Facts and citations are in S1 and S2.
S3 Recommendation#
| Persona | Pick | Avoid |
|---|---|---|
| Collaborative document editor | Yjs by default; Loro for trees and history; Automerge for JSON-first documents | Server-authoritative row engines; whole-document replication |
| Field-service app on flaky networks | PowerSync where the source is Postgres, MongoDB, MySQL or SQL Server; PouchDB with CouchDB; RxDB replication to an existing API | Zero (no offline writes), Triplit (stalled), Replicache (retired) |
| Multi-device notes or to-do app | CRDT documents with a thin relay (Yjs, Loro or Automerge); InstantDB or Jazz if hosted is acceptable | Engines that require running a Postgres or CouchDB for one user |
| Team dashboard with offline edits | PowerSync or ElectricSQL with an explicit write path; RxDB replication to an existing API; Zero only if users are online nearly all the time | CRDT-only stacks; PouchDB without CouchDB |
Cross-persona points#
- The CRDT libraries and the engines solve different problems; one app can use both (a CRDT for a document field, an engine for the rows around it).
- Offline-write support is the first filter, and Zero is the one engine that fails it by design.
- None of these replaces durable storage choices (1.118) or presence channels (1.067.2).
Persona: collaborative document editor#
Who needs this#
A team building a rich-text or structured-document editor where several people type into one document, sometimes offline on a plane, and expect all of their words to survive.
Why they need it#
Two people typing at the same position must both keep their text. A server that applies edits one at a time and lets the last write win would drop one of them. A merge rule built into the data type is the requirement, and it is exactly what the CRDT family provides (merge-probe: both concurrent inserts kept by Yjs, Automerge and Loro, measured 2026-10-05).
Requirements#
- Concurrent text edits that merge without data loss.
- Undo that affects only the local user’s edits.
- Offline editing of any length, then merge.
- Presence (cursors, who is online), which is a live-channel concern; see 1.067.2.
- Editor integration, and a sync server the team can run or replace.
What it eliminates#
- Zero and other server-authoritative row engines. A row is the wrong unit for a paragraph, and Zero rejects offline writes.
- ElectricSQL alone. It is read-path sync; the merge rule would be yours to write.
- PouchDB and RxDB replication. Whole-document conflicts (winner plus retained loser, or master-wins) would discard one author’s changes to the same document.
- Diamond Types. Text only, and npm packages from 2023.
What remains#
- Yjs. The widest editor bindings per its README, an UndoManager in the package, y-websocket and Hocuspocus as relays, 23,735 bytes gzipped (measured 2026-10-05). The default for a text editor.
- Loro. Rich-text and movable-tree types and version control; adds a WebAssembly file near 1.1 MB gzipped. Pick it for a document with structure (outlines, file trees) or when time travel through history is a feature.
- Automerge. JSON-first with its own repo and sync layer; pick it when the document is a nested record with text fields, not a stream of prose.
Constraints and trade-offs#
- No permissions inside a document. A CRDT document is shared whole. Per-section access is something the app has to build, for example as separate documents.
- History growth. Yjs garbage-collects by default and says turning that off is how you restore old content (README); Loro offers shallow snapshots. Growth over long histories was not measured here.
- A relay is still a server. It can be thin and ignorant of content, but someone runs it.
Decision criteria#
| If | Choose |
|---|---|
| The editor already has a Yjs binding and bytes matter | Yjs |
| The data has a tree or you need history browsing | Loro |
| The document is nested JSON more than prose | Automerge |
| You need row-level permissions inside one document | Split into several documents or add a sync engine |
Persona: field-service app on flaky networks#
Who needs this#
Technicians and inspectors who fill in forms, take photos and close jobs in basements, rural sites and elevators. Connectivity returns in bursts, sometimes after a full shift. The office owns the job records in a database it already runs.
Why they need it#
Every record entered offline must reach the office system without loss, in order, and with a clear rule when the office changed the same job meanwhile. Edits are mostly by one technician per record, so merging text is not the problem; durable queuing and a server that stays the source of truth are.
Requirements#
- Writes accepted with no connection for hours or days, stored durably, uploaded on reconnect.
- A server that applies or rejects each write (validation, business rules).
- Sync from the existing database, ideally with no schema rewrite.
- Per-user data scoping: a technician receives only their jobs.
- A conflict rule the office can reason about.
What it eliminates#
- Zero. Its documentation says writes are rejected while disconnected (https://zero.rocicorp.dev/docs/offline).
- Triplit. Stalled with no commits since January and under AGPL-3.0.
- Replicache. Maintenance mode.
- Plain CRDT libraries. They merge documents, but the job record must pass server validation, which a merge rule does not provide, and there is no per-user scoping.
- InstantDB for this role, until its offline writes are confirmed. The documentation read did not state it.
What remains#
- PowerSync. Offline writes queue in client SQLite and upload to a backend endpoint you control; the server is authoritative; data is partitioned per user; sources include Postgres, MongoDB, MySQL and SQL Server. Cost: a 160 kB gzipped client plus a SQLite WebAssembly file (measured 2026-10-05), and a service licensed FSL-1.1-ALv2, which bars offering a competing hosted service but permits running it for your own app.
- ElectricSQL with a persistent write queue or a local database. Postgres is required; offline writes depend on the pattern chosen (patterns 2 to 4 in its docs). More to build than PowerSync, and the client is smaller (18,019 bytes gzipped).
- RxDB replication. Your own pull and push endpoints, master-wins by default and a conflict handler hook; 54,953 bytes gzipped with the replication plugin. Confirm which RxDB plugins are premium.
- PouchDB with CouchDB. If the office system is or can be CouchDB, multi-master replication with retry is the oldest answer; the library’s last npm release is June 2024.
Constraints and trade-offs#
- Photos and files are not part of row sync in any of these; plan a separate upload path (1.118 covers local blob storage).
- iOS Safari can delete script-written storage after seven days without use for sites in a browser tab; installed apps are exempt (1.118).
- How long a device can stay away before a full resync was not measured for any engine.
Decision criteria#
| If the source database is | Choose |
|---|---|
| Postgres, MongoDB, MySQL or SQL Server and the team wants a packaged answer | PowerSync |
| Postgres and the team prefers to own the write path | ElectricSQL |
| CouchDB | PouchDB |
| An API the team already wrote | RxDB replication |
Persona: multi-device notes or to-do app#
Who needs this#
An independent developer or small team shipping a notes, checklist or journal app for one person across a phone, a laptop and a tablet. Few users per account, little appetite for operating a database, and a promise that data works with no connection.
Why they need it#
The same person edits on two devices that were both offline. The conflicts are rare and small (one note edited on two devices, a checkbox ticked on both), but losing a note is unacceptable. The server should be as thin as possible, or the user’s own cloud storage.
Requirements#
- Edits on any device with no connection, merged later without prompts.
- A cheap or no server.
- Text that merges (a note edited in two places).
- Small client weight on mobile web.
- End-to-end encryption is attractive; it requires a server that cannot read the data.
What it eliminates#
- Engines that require a Postgres or CouchDB you run (Electric, PowerSync, Zero, PouchDB): operations cost for a single-user app.
- Zero, again, for offline writes.
- Triplit and Replicache, per the durability signals.
What remains#
- CRDT documents plus a dumb relay. Yjs or Loro or Automerge: one document per note or per list, a relay that stores opaque update bytes, so the server needs no schema and cannot read content. Yjs is 23,735 bytes gzipped; Loro and Automerge add about 1.1 MB gzipped of WebAssembly (measured 2026-10-05). A to-do list is a good fit for Automerge’s JSON model or Loro’s movable list.
- InstantDB if the hosted service is acceptable. Apache-2.0 and self-hostable, query cache persisted on device; confirm offline writes before shipping.
- Jazz for a single-vendor path with groups and cloud storage; the 0.20 line and the 2.0 alpha differ, and the choice of line is the decision.
Constraints and trade-offs#
- Last-write-wins applies to scalar fields. In the probe, a title edited on two devices resolved to one value on all three libraries; the loser is silently dropped (Automerge reports it, Yjs and Loro do not expose it in the probe). For a title, acceptable; for a numeric counter, wrong.
- Search over notes needs a local index, which none of the CRDT libraries provides; see 1.118 for SQLite on OPFS and 1.003 for search.
- Storage eviction (1.118) still applies; keep an export.
Decision criteria#
| If | Choose |
|---|---|
| Bytes matter more than anything | Yjs |
| Lists with reorder, nested outlines | Loro |
| Notes with a history screen | Loro or Automerge |
| You want a ready-made backend and accept hosted | InstantDB or Jazz |
Persona: team dashboard with offline edits#
Who needs this#
An internal tool for a team of tens: a table of accounts, tasks or inventory rows backed by an existing Postgres database, with filters, inline edits, and the ability to keep working through a train ride or a flaky office network.
Why they need it#
The UI should respond instantly, show data other people changed without a refresh, and tolerate a dropped connection without losing the edit in progress. The database remains the system of record, and business rules (validation, who may edit what) must still apply when an offline edit finally arrives.
Requirements#
- Shared relational data with queries and filters on the client.
- Live updates from other users.
- Edits queue while offline and apply under server rules on reconnect.
- Permissions by team or role on which rows and columns arrive on the client.
- The UI layer (data grid) works against a local store.
What it eliminates#
- CRDT-only stacks. They merge documents but offer no relational queries, no row permissions, and no server-side rule check.
- Zero, if offline edits are required. It remains an excellent fit if the team is online nearly always and wants instant UI.
- PouchDB, unless the database is CouchDB.
- Triplit and Replicache for new work.
What remains#
- PowerSync. Client SQLite, rule-based data partitioning, an upload queue to your endpoint, and sources including Postgres. The server decides conflicts, which matches “database is the system of record.” Cost: 159,410 bytes gzipped of client JavaScript plus a SQLite WebAssembly file (measured 2026-10-05).
- ElectricSQL. Live shape subscriptions from Postgres over HTTP, authorization as a proxy you write, and one of the offline write patterns you pick. Client JavaScript is 18,019 bytes gzipped; the local store is yours (PGlite is the Postgres-in-browser option). Most flexible, most to build.
- RxDB replication. Reactive document queries with a replication plugin to your API; master-wins by default and a conflict handler hook; 54,953 bytes gzipped with replication.
- Zero, as the contrast: Postgres 15 or later with logical replication, a normalized client store, mutators that run on client and server, and Apache-2.0 code. Reads keep working offline; writes fail until reconnected.
Constraints and trade-offs#
- Permissions take most of the effort. Electric leaves them to you; PowerSync partitions data by rules; Zero uses context passed to queries and mutators. Check that column-level hiding is possible in the chosen engine.
- Conflict policy is a product decision. Two people editing one row offline: the server’s rule (reject, last write, field-level merge) must be written in your endpoint for PowerSync and Electric.
- Hosted versus self-run. PowerSync’s service is FSL-1.1-ALv2; Zero’s team sells it as a service; Electric’s docs moved to a new domain in 2026 and the hosted tier was not examined.
Decision criteria#
| If | Choose |
|---|---|
| The team wants a packaged offline queue and permissions by rule | PowerSync |
| The team prefers composing small parts and owns the write path | ElectricSQL |
| Online almost always; wants the fastest instant-UI model | Zero |
| The backend is an HTTP API with its own validation | RxDB replication |
S4: Strategic
S4 Approach: Viability and Lock-In#
S4 reads the durability signals from S2 against ownership, license and business model. It covers
viability per project (viability.md), the cost of leaving each (platform-risk.md), and
paths by risk appetite (recommendation.md).
Evidence base: npm registry, GitHub API (2026-10-05), project documentation pages cited in S1, and the Supabase announcement about Triplit. Forecasts beyond one to two years are not made; the category has reorganized within that horizon (Replicache retired, Triplit’s team moved, Jazz rewritten, Electric’s documentation host moved).
S4: Hosted Lock-In and the Cost of Leaving#
The tests#
- Can you run the server? Is there a self-hostable server under a license that permits running it for your app?
- Is the data model portable? Can the data leave as plain rows or JSON, or does it live in a proprietary format?
- Does the client talk to a different server? Can the client run against your own backend, as well as the vendor’s?
By project#
| Project | Self-host path | License of the server | Data portability | Leaving cost |
|---|---|---|---|---|
| Yjs, Automerge, Loro | You write or choose the relay | MIT | Update bytes plus a JSON export from the document | Low: the document converts to JSON; moving between CRDT libraries means a one-time migration |
| ElectricSQL | Yes | Apache-2.0 | Postgres stays the source of truth | Low: your database was never moved |
| PowerSync | Yes (Docker image) | FSL-1.1-ALv2 | Your source database stays the source of truth | Low to medium: the upload endpoint and sync rules are PowerSync-shaped |
| Zero | Yes | Apache-2.0 | Postgres stays the source of truth | Medium: mutators and query definitions are Zero-shaped |
| InstantDB | Yes (repository directory) | Apache-2.0 | Data stored as triples in Postgres | Medium: the triple store is not your schema |
| Jazz | Yes, per README | MIT | CoValues (classic) or relational (2.0) | Medium to high: the data model is Jazz’s |
| RxDB | Yes | Apache-2.0 core; premium plugins | Documents in your backend | Low to medium: replication endpoints are yours |
| PouchDB | Yes | Apache-2.0 | CouchDB documents | Low: the protocol is a standard shared by CouchDB servers |
| Triplit | Yes | AGPL-3.0 | Server-owned store | High: no active upstream |
| Replicache | Your backend implements the protocol | Custom terms | Your database | Medium: protocol is yours; client is frozen |
Findings#
- Databases that stay in your Postgres are cheaper to leave. Electric, PowerSync and Zero sync from a database you own; the thing you would replace is the sync layer.
- Own-database engines have the highest lock-in. Instant, Jazz and Triplit hold the data in their own model.
- License terms need reading, not assuming. PowerSync’s service is FSL-1.1-ALv2 rather than an OSI license; Triplit is AGPL-3.0; Replicache’s npm license field points to a terms page. “Open source” in a README is not a license check.
- A hosted-only product was not scored (see S1 neighbors) because the lock-in test cannot pass for it.
Keeping the choice reversible#
- Keep the source of truth in a database or file format you control, and treat the sync layer as replaceable.
- Put the sync library behind a thin interface in the app so that swapping is a one-module change.
- Pin versions; for automerge-repo, whose
latesttag is an alpha, pinning is necessary. - Re-run the stall test before each major decision; the category moved several times in a year.
S4 Recommendation#
Paths by risk appetite#
| Path | Pick | Why |
|---|---|---|
| Conservative | Yjs for documents; PowerSync or ElectricSQL over a Postgres you own for rows; PouchDB when CouchDB is already in place | Longest track records or standard protocols; data stays in a database you control |
| Performance-first | Loro for documents with structure; Zero where users are online nearly always | Rust-core CRDT with version control; instant online UI |
| Adaptive | CRDT documents with a thin relay, behind an interface the app owns | Least server lock-in; swap libraries or add an engine later |
Rules that hold across paths#
- Choose the conflict model before the library.
- Treat offline-write support as a pass/fail check against the documentation; Zero fails it by design and Instant’s is unconfirmed.
- Do not start new long-lived work on Triplit or Replicache.
- Pin automerge-repo, read the Jazz line (0.20 or 2.0) you are on, and read PowerSync’s FSL terms.
- Re-run the stall test annually; the category reorganized several times in a year (Replicache retired, Triplit’s team moved, Jazz rewritten, Electric’s documentation host moved).
Open items for a refresh#
- Offline duration limits (L4) and document growth (L5) were not measured; both would change recommendations at the margin.
- InstantDB offline-write behavior and Jazz 2.0’s conflict model need a docs read.
- RxDB’s premium boundary for replication plugins needs confirming.
S4: Viability by Project#
Signals are from S2 (measured 2026-10-05). Ownership and business model are from the project pages cited in S1.
Healthy#
- Yjs. First published 2015 and 130 commits since March. The ecosystem (editor bindings, providers) is the moat. Risk: provider packages age separately (y-indexeddb last released 2023-11-02).
- Automerge. 193 and 98 commits across the core and the repo layer. Risk: the sync layer’s npm
latesttag is an alpha. - Loro. 183 commits, 159 merged pull requests, MIT. Youngest of the three (first published 2023-04-07). Risk: age.
- ElectricSQL. 574 commits and 584 merged pull requests; Apache-2.0. Reorganized: documentation moved to electric.ax. Its funding model was not examined.
- PowerSync. 221 and 248 commits across service and SDK. The service license (FSL-1.1-ALv2) bars competing hosted offerings and converts to Apache-2.0 after a delay.
- Zero (and Rocicorp). 830 commits. Rocicorp says it earns money by running Zero as a service and commits to the core staying Apache-2.0. Risk: it does not support offline writes and says that is not a priority.
- RxDB. 1,276 commits; core is Apache-2.0 with premium plugins. Risk: premium boundary.
- InstantDB. 415 commits; Apache-2.0; self-hosting path exists but the hosted service is the default.
In transition#
- Jazz. A rewrite (2.0 alpha) with a new relational API alongside a maintained classic line. Pick one line and expect migration.
- PouchDB. Alive (96 commits, 46 merged) but the last npm release was June 2024 and the pull-request queue is long.
- Diamond Types. Live source, 2023 packages, text only.
Not for new work#
- Triplit. Stalled (0 commits and 0 merges since 2026-03-01); its co-founder joined Supabase (2025-10-08), which said it would not integrate Triplit.
- Replicache. In maintenance mode by its authors’ announcement.
Concentration risk#
Zero’s authors state that they fund the open code by selling Zero as a service (source: https://zero.rocicorp.dev/docs/open-source, accessed 2026-10-05). The funding models of Electric, PowerSync, RxDB, Instant and Jazz were not examined. The CRDT libraries are stewarded by communities or small teams, so their risk is maintainer concentration, not a business model. Before depending on any of them, check whether a second person holds release rights.