Compared with alternatives
syncom is a data engine that keeps records on the device and replicates them between nodes. That puts it where four kinds of tools meet: the CRDT libraries (Yjs, Automerge, Loro, Jazz), which merge documents and leave storage and queries to you; the local-first databases (RxDB, PouchDB, TinyBase, Dexie Cloud, Fireproof), which store and query records and sync them through a backend; the peer-to-peer and event-sourced stores (GUN, OrbitDB, Evolu, LiveStore); and the sync services (ElectricSQL, PowerSync, Zero, Triplit, InstantDB), which replicate a server database to clients. The table compares syncom with eighteen of them, with every feature in one table rather than a few favourable rows. syncom is the first column. It is tinted, and on a wide screen it stays pinned next to the features while the table scrolls, so every row can be read against it.
How fast each one is, measured on the same scenarios, is on the Benchmarks page. RxDB, the closest of them, is compared with syncom feature by feature, in 300 rows, on Compared with RxDB.
What the table shows
- A database or a merge, rarely both. The CRDT libraries merge text and lists character by character, but they have no queries, no indexes and no schema (Loro and Jazz have partial ones). The databases and services do query, but most of them resolve a conflict per document or per revision, or let the server decide. syncom sits between the two. It gives you records with a schema, Mango queries, indexes and unique keys, and it merges concurrent writes field by field, with counters that add up. That makes it a database that merges, not a document CRDT: it has no text or sequence type.
- Few sync without a server of their own and still check what they receive. Nine of the nineteen need no server from the project to sync: syncom, Yjs, Automerge, Loro, RxDB, PouchDB, TinyBase, GUN and OrbitDB. Of those nine, only syncom and RxDB validate writes against a schema. Only RxDB and PouchDB let the node serving the data reject a write before it spreads (RxDB through its server plugin, PouchDB through CouchDB's
validate_doc_update). A syncom host runs the same validators, hooks and scopes on a peer's op that it runs on its own writes, but it still relays an op its validator or hook skipped, so every node behind it judges that op for itself; only out-of-scope and unsigned ops stop at the host. - Counters that converge are rare. syncom, Automerge, Loro and Dexie Cloud have them; Dexie Cloud replays the operation on its server. Everywhere else, two replicas that increment the same number at the same time lose one of the increments.
- A history that cannot be altered usually cannot be purged either. Five keep a tamper-evident history: syncom, Automerge, Jazz, Fireproof and OrbitDB. Automerge, Fireproof and OrbitDB cannot take a payload back out of it. syncom's
purge()removes the payload from the op log and keeps the record of the operation; Jazz erases a CoValue on its server. Zero, Triplit, InstantDB and TinyBase can delete for good because they keep no history to begin with. - Unique keys across replicas. Only syncom and InstantDB enforce a unique key. syncom checks it on every write and reports a clash between two peers as a
conflictevent. In the others the key is per device, or it is checked only where the server writes to Postgres. - Where syncom is behind. It has no collaborative text and no list CRDT (Yjs, Automerge, Loro and Jazz do). End-to-end encryption is a recipe, field by field (Jazz signs and encrypts every change; Evolu encrypts). There are no UI bindings and no React Native target, and it is not yet on npm.
- Some are no longer moving, or are moving away. PouchDB has not published since June 2024, and GUN's npm release is from July 2025. Triplit's last release was July 2025 and its site is gone. Instant Cloud closed to new signups and shuts down on 2027-08-31; InstantDB stays open source for self-hosting. Jazz's npm
latest(0.20) is not the version its site documents (2.0, in alpha).
How the table was made
- From the code, not the README. Each library was read at its latest npm version in October 2026: the published tarball, plus the official docs. Where the docs and the code disagree, the cell follows the code and a footnote says so. A service is judged as client and server together; the cell says which side a feature lives on.
- What ✅ means. A feature counts as ✅ only when it ships in the project's own packages, and the package is named in the cell. A third-party package, a paid tier or an experimental flag makes it ⚠️, with the reason given.
- The syncom column is this repository at the version on the page, checked against its tests.
- The data is in the repository.
docs/comparison.jsonkeeps every cell with the file or page it was decided from, so you can check any of them.pnpm docs:comparisonrenders it into this page. If a cell is wrong, open an issue or edit the data; the table is generated from it.
The matrix
| Feature | syncom sync engine | Yjs CRDT library | Automerge CRDT library | Loro CRDT library | Jazz local-first framework | RxDB local-first DB | PouchDB local-first DB | TinyBase reactive store | Dexie Cloud IndexedDB + cloud | Fireproof ledger DB | GUN p2p graph DB | OrbitDB p2p DB | Evolu local-first SQLite | LiveStore event-sourced SQLite | ElectricSQL Postgres sync | PowerSync Postgres/Mongo sync | Zero query sync | Triplit sync DB | InstantDB sync backend |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Package | |||||||||||||||||||
| Version compared | 0.0.1 (@alexify/syncom) | 13.6.33 (yjs) | 3.5.0 (@automerge/automerge); repo 2.6.0-alpha.31 | 1.16.4 (loro-crdt) | 0.20.19 (jazz-tools)2 | 17.6.0 (rxdb) | 9.0.0 (pouchdb) | 10.0.1 (tinybase) | dexie 4.4.6 + dexie-cloud-addon 4.4.163 | 0.24.19 (@fireproof/core, use-fireproof) | 0.2020.1241 (gun, SEA and RAD included) | 4.0.0 (@orbitdb/core) | 8.23.2 (@evolu/common) | 0.5.0 (@livestore/livestore, beta) | @electric-sql/client 1.5.28; server 1.8.1 | @powersync/web 2.5.0 (node 1.2.0, react-native 2.3.3) | 1.11.0 (@rocicorp/zero) | @triplit/client 1.0.50 (db 1.1.10, server 1.1.8) | 1.0.67 (@instantdb/core, /react, /admin) |
| Latest release | not yet on npm (pre-release) | 2026-09 | 2026-09 | 2026-09 | 2026-07 | 2026-10 | 2024-064 | 2026-09 | 2026-10 (addon), 2026-09 (dexie) | 2026-04 | 2025-075 | 2026-05 | 2026-10 | 2026-10 | 2026-09 | 2026-10 | 2026-10 | 2025-07, no release since6 | 2026-087 |
| License | MIT | MIT (providers and server MIT too) | MIT (sync server MIT too) | MIT (protocol packages MIT too) | MIT (jazz-run server MIT); Jazz Cloud managed | Apache-2.0; Premium plugins paid8 | Apache-2.0 (CouchDB too) | MIT | Apache-2.0 client; server commercial9 | AFL-2.0 per npm; Apache-2.0 file10 | Zlib OR MIT OR Apache-2.0 | MIT | MIT (client and relay) | Apache-2.0 | Apache-2.0, client and server | Apache-2.0 client; service FSL-1.1-ALv211 | Apache-2.0 (client and zero-cache) | AGPL-3.0: client, db and server12 | Apache-2.0 (client and server) |
| Runtime dependencies | ✅ 0 | 1 — lib0 (same author) | ✅ 0 (automerge-repo: 8) | ✅ 0 | 18 — zod, cojson, ProseMirror… | 24 direct + required peer rxjs; 74 installed | 14 (pouchdb); 3 (pouchdb-browser) | ✅ 0 (integrations are optional peers) | 5 (dexie-cloud-addon); dexie 0 | 5 (@fireproof/core) | 1 (ws) + optional @peculiar/webcrypto | 10 (+ Helia/libp2p you supply) | 5 (kysely, @noble/*, @scure/bip39, random) | 3 (+ peer effect) | 1: @microsoft/fetch-event-source | 5 (wa-sqlite, comlink, commander, …) | 48 (client and zero-cache ship together)13 | 4: @triplit/db, logger, comlink, superjson | 3: uuid, mutative, @instantdb/version |
| Implementation | JS (CommonJS), no build step | JS (ESM + CJS build), JSDoc types | Rust → WASM, TS wrapper | Rust → WASM | TS + Rust crypto core (WASM/N-API/RN) | TypeScript | JavaScript | TypeScript | TS client; closed-source server | TypeScript | JS (hand-written UMD, no build) | JS (ESM) | TS; SQLite (WASM / native) | TS on Effect; SQLite WASM | Elixir service + TS client | TS SDK + Rust SQLite extension; TS service | TypeScript; zero-cache on Node + SQLite | TypeScript client + Node/Bun/Cloudflare server | TS/JS client + Clojure server on Postgres |
| Runtimes | |||||||||||||||||||
| Node.js, as a replica | ✅ ≥22, node:fs | ✅ y-leveldb + y-websocket client; Node ≥16 | ✅ storage-nodefs + WebSocket adapter; Node ≥22.13 (repo) | ⚠️ runs (WASM); persistence is app code | ✅ workers, jazz-run sync (SQLite); Node ≥20 | ⚠️ ≥20; free persistence via MongoDB/FoundationDB storages14 | ✅ LevelDB via native leveldown | ✅ file, node:sqlite, Postgres persisters | ⚠️ IndexedDB shim only; not documented | ✅ files under ~/.fireproof | ✅ RAD files on disk; engines ≥0.8.4 | ✅ ≥22; LevelDB + Helia blockstore | ❌ no Node client; relay only (≥24.20) | ✅ @livestore/adapter-node (Node, Bun, Deno) | ✅ client + PGlite on filesystem; no engines15 | ✅ @powersync/node, better-sqlite3; no min declared | ⚠️ client in Node undocumented; memory fallback | ✅ client + sqlite/lmdb storage; engines ≥18 | ❌ @instantdb/admin: online HTTP/SSE, no local store16 |
| Browser persistence | ✅ OPFS, IndexedDB (/idb), memory | ✅ IndexedDB (y-indexeddb) | ✅ IndexedDB (storage-indexeddb) | ⚠️ none built in — app stores exported bytes | ✅ IndexedDB (cojson-storage-indexeddb) | ✅ IndexedDB (via Dexie), localStorage; OPFS Premium | ✅ IndexedDB; memory, localStorage adapters | ✅ IndexedDB, OPFS, localStorage, SQLite WASM | ✅ IndexedDB | ✅ IndexedDB (encrypted CAR files) | ✅ localStorage default; IndexedDB via lib/rindexed | ✅ IndexedDB via level + Helia blockstore | ✅ SQLite WASM on OPFS, encrypted | ✅ OPFS only; in-memory fallback | ✅ PGlite: IndexedDB, OPFS; client in-memory | ✅ SQLite: IndexedDB (default), OPFS, memory | ✅ IndexedDB (default) or memory | ✅ IndexedDB (cached in memory), memory17 | ⚠️ IndexedDB cache: recent queries, pending txs |
| React Native / mobile | ❌ not a target; fsi is injectable | ⚠️ pure JS; RN storage third-party (y-op-sqlite) | ⚠️ Swift/Java bindings; React Native not documented | ⚠️ Swift (loro-swift); RN binding outside loro-dev org | ✅ React Native and Expo (SQLite) | ⚠️ React Native/Expo via Premium SQLite or filesystem | ⚠️ third-party pouchdb-adapter-react-native-sqlite | ✅ Expo SQLite, RN MMKV/SQLite, Capacitor | ⚠️ Capacitor/Electron WebView; no React Native | ❌ no React Native support documented | ⚠️ wiki recipe: gun/lib/mobile + AsyncStorage (lib/ras) | ❌ not documented | ✅ @evolu/react-native (Expo, SQLCipher) | ✅ @livestore/adapter-expo (expo-sqlite) | ✅ React Native/Expo: client react-native export | ✅ @powersync/react-native (op-sqlite), @powersync/capacitor | ✅ React Native/Expo: expo-sqlite, op-sqlite stores | ✅ @triplit/react-native (Expo), expo-sqlite storage | ✅ @instantdb/react-native (AsyncStorage, MMKV, expo-sqlite) |
| Server needed to sync | ✅ none — any node can host | ✅ none with y-webrtc; @y/websocket-server relay18 | ✅ none required; automerge-repo-sync-server relay19 | ✅ none required; loro-websocket SimpleServer | ⚠️ a sync server: Jazz Cloud or jazz-run sync | ✅ any backend: own endpoints, CouchDB, GraphQL, Supabase… | ✅ CouchDB-compatible, self-hostable; or none | ✅ none (BroadcastChannel) or WsServer relay | ⚠️ Dexie Cloud SaaS or paid on-prem | ⚠️ Fireproof Cloud; self-host undocumented20 | ✅ none; any GUN peer relays (gun/lib/server) | ✅ none; libp2p relay for browsers | ⚠️ relay needed; self-host @evolu/relay / Docker21 | ⚠️ central backend: Cloudflare DO, Electric, S2, custom | ⚠️ Electric service + Postgres ≥14; self-hostable | ⚠️ PowerSync Service + source DB + your API | ⚠️ zero-cache + Postgres 15+ + your API | ⚠️ @triplit/server required; self-host only | ⚠️ Instant server + Postgres; Docker self-host or Cloud |
| Data model | |||||||||||||||||||
| Data model | JSON records in collections | shared types: Y.Map, Y.Array, Y.Text, XML | JSON-like CRDT documents, many per Repo | JSON-like CRDT doc: Map, List, Text, Tree, Counter | CoValues (maps, lists, feeds, text) in a graph | JSON documents in collections | JSON docs with revision trees | in-memory tables + key-values | IndexedDB tables; Y.Doc properties | JSON docs in a Merkle CRDT ledger | graph of nodes (souls) with fields | append-only log; events, KV, documents | SQLite tables; LWW per column | event log → SQLite tables (materializers) | Postgres table rows, synced as shapes | SQLite tables (views over JSON rows) | Postgres tables (rows) with relationships | documents in collections, schema-defined relations | entities in namespaces + links (triples in Postgres) |
| Schema validation of writes | ✅ metaschema, Standard Schema, ajv; local writes and peers' ops22 | ❌ none | ❌ none — TS generics only | ⚠️ loro-mirror DSL; local setState only | ⚠️ Zod-based co.* schemas; runtime check defaults to warn23 | ✅ JSON Schema via validate-ajv, validate-z-schema24 | ❌ none client-side | ⚠️ cell types + defaults; zod etc. converted25 | ❌ index spec only, no validation | ❌ none | ❌ none | ❌ none | ⚠️ Standard Schema types; app validates input26 | ✅ Effect Schema per event, encoded on commit | ⚠️ Postgres constraints; Standard Schema via TanStack DB | ⚠️ Schema declares views; raw-table SQLite constraints27 | ⚠️ mutator args via Standard Schema; schema.ts types only28 | ✅ own DSL Schema as S; optional (schemaless mode) | ✅ own DSL i.schema/i.entity, enforced by server |
| Schema versions and migrations | ✅ version on every op; lens on read; migrate() | ❌ none | ❌ none | ❌ none | ⚠️ withMigration() on every load; no versions | ✅ version + migrationStrategies | ❌ none | ❌ no versioned schema | ⚠️ version().upgrade(), unsynced tables only | ❌ none; normalize in index functions | ❌ none | ❌ none | ⚠️ append-only schema; no migration functions | ✅ state rebuilt from the eventlog | ⚠️ your Postgres migrations; DDL forces shape re-sync29 | ⚠️ none needed for JSON views; no migration API | ⚠️ Postgres DDL; expand/contract deploy order | ⚠️ schema push with compatibility checks; no migrations | ⚠️ instant-cli push schema diff + renames; no data migrations |
| Queries | ✅ Mango select(), order, top-k | ❌ none — app reads shared types | ❌ none — load documents by URL | ⚠️ JSONPath (RFC 9535) over one doc | ❌ none — traverse references from known ids | ✅ Mango (mingo); query-builder plugin | ✅ Mango (pouchdb-find), map/reduce | ✅ queries module (TinyQL: select, join, group) | ✅ where() on indexes + filter() | ✅ map-function indexes, key ranges | ⚠️ graph traversal; LEX key ranges via RAD | ⚠️ query(fn) scans the whole log30 | ✅ typed SQL via Kysely | ✅ SQL + query builder (SQLite) | ✅ SQL where per shape; local SQL via PGlite | ✅ SQL (SQLite); Drizzle, Kysely drivers | ✅ ZQL builder; named queries resolved by your API | ✅ own builder: Where, Order, Include, subqueries | ✅ InstaQL (nested JSON: where, order, cursors) |
| Secondary indexes | ✅ entity index fields, equality only, in memory | ❌ none | ❌ none | ❌ none | ❌ none | ✅ schema indexes, compound | ✅ Mango indexes, map/reduce views | ✅ indexes module (slices) | ✅ compound, multi-entry | ✅ persistent prolly-tree indexes | ❌ none; app-built index nodes | ❌ none; keyvalue-indexed indexes keys only | ✅ indexes config (Kysely builder) | ✅ table indexes | ✅ PGlite CREATE INDEX; client has none | ✅ indexes in Table options | ✅ automatic: Postgres indexes mirrored; client in-memory | ❌ collection scan or id lookup only | ✅ .indexed() attributes, built on the server |
| Unique constraints | ✅ unique keys; a peer's clash is a conflict event | ❌ none | ❌ none | ❌ none | ⚠️ getOrCreateUnique: deterministic id per group | ❌ primary key only | ❌ _id only | ❌ row id only | ⚠️ & index per device; sync not documented | ❌ _id only | ❌ none | ❌ only the indexBy key | ❌ rejected by design | ⚠️ isUnique index, per client only31 | ⚠️ Postgres UNIQUE, when your API writes | ⚠️ source DB on upload; raw tables locally | ⚠️ Postgres enforces; .unique() only declares | ❌ only the id key | ✅ .unique(), enforced by the server |
| Live queries / change events | ✅ watch(), change events | ✅ observe/observeDeep, doc update events | ✅ DocHandle change events with patches | ✅ subscribe, subscribeJsonpath | ✅ subscribe, useCoState hooks | ✅ $ observables (event-reduce) | ⚠️ changes({ live: true }) feed | ✅ listeners at every level; UI hooks | ✅ liveQuery() | ✅ subscribe(), useLiveQuery | ✅ .on() on any node or path | ✅ events.on('update'); no live queries | ✅ subscribeQuery, useQuery | ✅ queryDb, signals, computed | ✅ ShapeStream, Shape.subscribe, useShape | ✅ query().watch(), differentialWatch(), onChange | ✅ useQuery, materialize(); incremental view maintenance | ✅ client.subscribe, useQuery; incremental view maintenance | ✅ every query is live: useQuery, subscribeQuery |
| Collaborative text | ❌ none | ✅ Y.Text (rich), Y.XmlFragment; editor bindings | ✅ text with marks and blocks (Peritext) | ✅ LoroText rich text; ProseMirror/CodeMirror bindings | ✅ co.plainText(), co.richText(); ProseMirror | ❌ none | ❌ none | ❌ none | ✅ Y.Doc props via y-dexie (Yjs) | ❌ none | ❌ none | ❌ none | ❌ none | ❌ pair with Yjs / Automerge / Loro | ⚠️ Yjs via @electric-sql/y-electric provider | ❌ none built in; Yjs-as-rows demo | ❌ no text CRDT | ❌ no text CRDT; S.String is a register | ❌ none; strings are last-write values |
| Ordered lists that merge | ❌ no ordered list; an array field can be an observed-remove set (add()/remove()) | ✅ Y.Array (YATA sequence) | ✅ lists with insertAt/deleteAt | ✅ LoroList, LoroMovableList, LoroTree | ✅ co.list() (ordered CRDT) | ⚠️ crdt plugin replays $push | ❌ arrays are part of the doc | ❌ array cells are LWW | ⚠️ Y.Array; add() sorts plain arrays32 | ❌ arrays are part of the doc | ❌ set() is unordered (random keys) | ⚠️ events log: append-only, merged order | ❌ none | ⚠️ fractional indexing (Rocicorp package) | ⚠️ only inside a Yjs doc; columns replaced whole | ❌ rows, or a JSON column replaced whole | ❌ no sequence CRDT; rows or json() columns | ❌ S.Set only; arrays are replaced whole | ❌ arrays are JSON values; links are sets |
| Counters that converge | ✅ inc(): per-origin sums33 | ❌ no counter type34 | ✅ Counter type | ✅ LoroCounter | ❌ no counter type | ⚠️ crdt plugin $inc | ❌ one revision wins | ❌ cells are LWW | ✅ add(n)/remove(n) replayed on server | ❌ one winner per _id | ❌ LWW fields; concurrent increments overwrite | ❌ no counter store in v4 | ❌ LWW columns only | ⚠️ increment events + your materializer | ❌ not built in; DIY in your API | ❌ no; PATCH uploads absolute values | ⚠️ server re-runs mutator; no counter type35 | ❌ numbers are last-write registers | ❌ no increment op; read-modify-write |
| Replication | |||||||||||||||||||
| Topology | mesh; hubs that dial each other | any — client–server or WebRTC mesh | any — client–server or peer-to-peer | any — app moves bytes | client–server through sync servers | client–server; P2P over WebRTC | multi-master, any pair of DBs | relay hub, local peers, custom | client–server only | hub: cloud or custom gateway | mesh (DAM) with relay peers | p2p mesh (libp2p pubsub) | client–relay; several relays at once | client–server (central backend) | client–server only; CDN fan-out | client–server only (service + your backend) | client–server: Postgres → zero-cache → clients | client–server only, one sync server | client–server only |
| Transport | ✅ injected link; wrpc WebSocket/WebRTC | ✅ providers: WebSocket, WebRTC; y-protocols sync | ✅ adapters: WebSocket, MessageChannel, BroadcastChannel | ✅ any (bytes); loro-websocket | ⚠️ WebSocket (cojson-transport-ws) | ✅ pluggable: HTTP, WebSocket, WebRTC, GraphQL… | ⚠️ HTTP (CouchDB protocol); socket-pouch third-party | ✅ WebSocket, BroadcastChannel, custom | ⚠️ HTTPS + WebSocket, fixed | ✅ pluggable gateways; cloud over WS/HTTP | ✅ WebSocket, WebRTC, UDP multicast; adapters | ✅ libp2p transports (TCP, WS, WebRTC…) | ⚠️ WebSocket only | ✅ pluggable SyncBackend; WS / HTTP in sync-cf | ⚠️ HTTP only: long-poll or SSE | ⚠️ HTTP streaming or WebSocket; not pluggable | ⚠️ fixed: WebSocket to zero-cache, HTTP to your API | ⚠️ WebSocket; transport option undocumented | ⚠️ WebSocket, SSE fallback; not pluggable |
| Browser to browser (WebRTC) | ✅ wrpc PeerLink + PeerHost | ✅ y-webrtc (signaling server only)36 | ⚠️ no official adapter; automerge-repo-network-peerjs (third-party) | ⚠️ possible; no official adapter | ❌ not built in | ✅ replication-webrtc (simple-peer) | ❌ no maintained plugin | ❌ none built in | ❌ none | ❌ none built in | ✅ gun/lib/webrtc (relay for signaling) | ⚠️ via @libp2p/webrtc + circuit relay | 🔜 P2P planned (Protocol docs) | ❌ central backend required | ❌ every client reads from the Electric service | ❌ no; all data flows through the service | ❌ no peer-to-peer | ❌ sync always goes through the server | ❌ no; even presence/topics relay through the server |
| Partial replication | ✅ scopes per peer, enforced by the host on the record | ⚠️ per document (room); subdocuments37 | ⚠️ per document; sharePolicy | ⚠️ per document (room) | ✅ per CoValue, loaded on demand | ⚠️ one replication per scope; backend filters | ✅ filter, doc_ids, selector | ❌ whole store per path | ✅ realms the user can access | ❌ whole ledger | ✅ on demand: only nodes you get | ❌ whole database log | ✅ per owner (useOwner, shard owners) | ⚠️ per store (storeId) only | ✅ shapes: table, where, columns, subset snapshots | ✅ Sync Streams (subscriptions with params, TTL) | ✅ per-query sync; TTL up to 10 min | ✅ per-query sync, filtered by read permissions | ✅ per query; view rules filter results |
| Resume after a disconnect | ✅ from the last event id | ✅ state-vector exchange on reconnect | ✅ sync state per peer, persisted | ✅ export({ mode: 'update', from }); rejoin by version | ✅ known-state diff per CoValue | ✅ checkpoints | ✅ checkpoints in _local docs | ✅ hash-tree diff on reconnect | ✅ delta since last server revision | ✅ clock heads; missing CAR files fetched | ⚠️ re-sends live gets with value hashes | ✅ heads exchange; fetch only missing entries | ✅ range-based set reconciliation | ✅ pull from cursor (seqNum) | ✅ offset + handle per shape | ✅ from last op id per bucket | ✅ base cookie + server CVR, kept 7 days | ✅ per-query checkpoint: server timestamp + cached ids | ⚠️ queries re-sent; server returns full results |
| Bootstrap a new replica | ✅ streamed snapshot past the log's retention | ✅ state-vector diff of merged state | ✅ whole compressed doc to an empty peer38 | ✅ snapshot; shallow snapshot trims history | ❌ no snapshots — full history per CoValue | ✅ state-based: pulls current docs | ✅ changes feed: leaf revisions only | ✅ state-based: diff by content hashes | ✅ server snapshot of the user's realms | ⚠️ compact() merges CAR files, keeps clock39 | ✅ state-based; no history to replay | ❌ newcomer traverses the full log | ❌ fetches the full change history | 🔜 full eventlog replay; compaction planned (#136) | ⚠️ shape log starts with a snapshot; compaction opt-in40 | ⚠️ compacted bucket history, not a snapshot | ✅ clients hydrate current query results, no history | ✅ current query results, no history replay | ✅ server sends current query results |
| Conflict resolution | LWW per field by (stamp, origin); counters add | CRDT (YATA); Y.Map key: higher clientID wins41 | CRDT; map key LWW by op id, losers kept42 | CRDT; Map LWW by Lamport, then peer id | LWW per key by wall-clock madeAt; CoList CRDT43 | custom conflictHandler; default master wins | deterministic winner; _conflicts kept | LWW per cell (HLC) | server-authoritative; LWW per property44 | Merkle clock; deterministic winner per _id | HAM: LWW per field, wall-clock state45 | LWW per key (Lamport clock, writer id) | LWW per column (HLC)46 | backend total order + client rebase47 | server authority: your API writes Postgres | server authority: your backend decides48 | server authority; client rebases pending mutations49 | attribute patches; the last to reach the server wins50 | server order, last write per attribute; merge for JSON51 |
| Offline writes | ✅ local op log, kept until the hosts have it52 | ✅ local edits merge on reconnect (y-indexeddb) | ✅ local changes merge on reconnect | ✅ local edits merge on import | ✅ local-first; merges on reconnect | ✅ local writes, pushed later | ✅ local DB, replicate later | ✅ MergeableStore merges later | ✅ local IndexedDB, synced later | ✅ local ledger, synced later | ⚠️ local write; in-memory send queue53 | ✅ local append; heads exchanged on join | ✅ local SQLite; synced later | ✅ local eventlog; pushed after rebase | ⚠️ DIY patterns; @tanstack/offline-transactions (TanStack) | ✅ local SQLite + upload queue (uploadData) | ❌ rejected once disconnected (default after 60 s)54 | ✅ durable outbox, optimistic, rebased on server data | ✅ queued in IndexedDB, replayed on reconnect |
| A server can reject a write before it spreads | ⚠️ out-of-scope and unsigned ops stop at the host; one its validator or beforeApply skips is relayed55 | ⚠️ none in @y/websocket-server; Hocuspocus hooks (third-party) | ⚠️ per-document access only; no change validation | ⚠️ authenticate → read/write per room; no op checks | ⚠️ signature/role checks; content opaque to server56 | ✅ backend is master; RxServer changeValidator | ✅ CouchDB validate_doc_update | ❌ relay forwards client to client57 | ⚠️ permissions enforced; no custom validation | ❌ cloud stores blocks and meta only | ⚠️ relay middleware can drop; SEA checks signatures | ⚠️ every peer checks canAppend + signature58 | ❌ relay is blind (E2EE) | ✅ onPush can reject a batch (sync-cf) | ✅ your API is the only write path | ✅ writes reach others only via your backend | ✅ server mutators in a Postgres transaction | ✅ server re-checks schema and permissions; can reject | ✅ server applies every tx; rules can reject |
| Integrity and security | |||||||||||||||||||
| Tamper-evident history | ✅ hash-linked chain, unsigned (optional)59 | ❌ none | ✅ SHA-256 change hash graph; checked on load | ❌ checksums detect corruption only | ✅ BLAKE3 hash chain per session | ❌ none | ❌ none | ❌ hashes serve sync diffs only | ❌ none | ✅ SHA-256 content-addressed Merkle clock | ⚠️ signed fields, # content-hash souls; no log | ✅ Merkle-DAG of CIDs (sha256, dag-cbor) | ⚠️ AEAD per change; no hash chain | ❌ none | ❌ not built in | ❌ bucket checksums catch corruption, not tampering | ❌ not built in | ❌ no hash chain | ❌ none |
| Signed changes | ✅ Ed25519 per op; the node id is the key; 'require' to enforce60 | ❌ none | ❌ none — author is unsigned metadata | ❌ none | ✅ Ed25519 per session | ❌ none | ❌ none | ❌ none | ❌ none | ❌ none; cloud uses JWT auth | ✅ SEA signs user-space (~pubkey) writes61 | ✅ every entry signed by its identity | ❌ shared owner key, no signatures | ❌ none | ❌ not built in | ❌ JWT per connection; changes unsigned | ❌ not built in; auth via cookie/token | ❌ JWT per connection, not per change | ❌ none; auth by session token |
| Encryption at rest | ⚠️ per record (encrypted: true), never synced62 | ❌ none — y-indexeddb stores raw updates | ❌ none | ❌ none — storage is the app's | ⚠️ stored encrypted; keys kept locally63 | ⚠️ encryption-crypto-js per field; WebCrypto Premium64 | ⚠️ third-party crypto-pouch | ❌ none | ⚠️ dexie-encrypted (beta for Dexie 4) | ✅ AES-GCM by default | ⚠️ manual SEA.encrypt; stores plaintext otherwise | ⚠️ encryption module encrypts stored entries | ✅ SQLCipher (web, Electron, Expo) | ❌ not supported (issue #70) | ❌ not built in | ⚠️ opt-in encryptionKey; Node needs cipher fork | ⚠️ op-sqlite encryptionKey on React Native only | ❌ no local encryption | ❌ IndexedDB cache in plaintext |
| End-to-end encryption | ⚠️ recipe: wrpc's HPKE sealer in hooks, per field | ⚠️ y-webrtc: DTLS + room password | ❌ none (Keyhive pre-alpha, separate) | ⚠️ %ELO via EloAdaptor (experimental)65 | ✅ XSalsa20 with group read keys | ❌ replication reads decrypted docs66 | ⚠️ third-party comdb (beta) | ❌ none | ⚠️ dexie-encrypted above the sync layer67 | ⚠️ blocks encrypted; key sent with meta68 | ⚠️ manual SEA.encrypt / SEA.secret (ECDH) | ⚠️ @orbitdb/simple-encryption, unaudited69 | ✅ XChaCha20-Poly1305, always on | ❌ DIY via schema transform (issue #70) | ❌ app-level; official example only | ❌ DIY: sync app-encrypted values | ❌ no; zero-cache runs queries on plaintext rows | ❌ server reads all data | ❌ no; server reads all data for rules |
| Access control where data is served | ✅ host scopes for reads and writes, authorize | ⚠️ per room, in your WebSocket upgrade handler | ⚠️ shareConfig.access per peer/doc (experimental)70 | ⚠️ per-room read/write via server authenticate | ✅ Groups: reader/writer/admin, cryptographically enforced | ✅ RxServer authHandler, queryModifier, changeValidator (rxdb-server)71 | ⚠️ CouchDB per-database roles72 | ⚠️ authenticate the WS upgrade per path | ✅ realms, roles; table/row/property rules | ⚠️ per-ledger tokens via Fireproof Cloud | ⚠️ owner-only user space, SEA.certify; reads open | ⚠️ write ACLs; no read control73 | ⚠️ owner keys; write-key token; relay isOwnerAllowed | ⚠️ DIY: validatePayload, onPush / onPull | ⚠️ your authorizing proxy; API open by default74 | ✅ service: JWT + stream queries; backend: writes | ✅ your query/mutate endpoints, from server context | ✅ roles + per-collection filters, server-enforced | ✅ CEL-style rules in instant.perms.ts, server-side75 |
| Purge: the payload leaves history | ✅ purge() scrubs the op log76 | ⚠️ deleted content GC'd (gc: true default)77 | ❌ history kept; delete() is local only | ⚠️ delete + shallow snapshot; redactJsonUpdates78 | ✅ deleteCoValues (admin), erased in background | ⚠️ cleanup plugin purges tombstones79 | ⚠️ purge(), indexeddb adapter only80 | ✅ no history; a delete keeps no payload | ⚠️ DELETE /users/:id; records not documented | ❌ no API; Merkle history immutable | ⚠️ put(null) overwrites; opt-in lib/erase | ❌ immutable log; del appends a tombstone | 🔜 owner deletion planned; deleteOwner is a stub81 | ❌ append-only eventlog | ⚠️ Postgres DELETE; shape logs keep the payload82 | ⚠️ delete at source; compact drops old payloads | ✅ hard DELETE; no history kept | ✅ hard delete; no history to scrub | ✅ delete removes the triples; no history kept |
| History / time travel | ⚠️ op log within retention; repair(id); no read-at-version | ⚠️ Y.snapshot; needs gc: false | ✅ view(doc, heads), diff, getHistory | ✅ checkout, forkAt, revertTo | ✅ $jazz.getEdits(); branches (unstable) | ⚠️ latest revision; the crdt plugin keeps every op in the document | ⚠️ get(id, { rev }) until compaction | ⚠️ checkpoints: in-memory undo/redo | ❌ no API | ⚠️ changes(since); no read-at-version | ❌ latest value per field only | ⚠️ full oplog via log.iterator() | ✅ evolu_history table, queryable | ⚠️ eventlog via store.events() | ❌ no; replica=full adds old_value only | ❌ current checkpoint only | ❌ not built in | ❌ current state only | ❌ no versions; nightly backups only |
| Extensibility and operations | |||||||||||||||||||
| Write hooks / middleware | ✅ open({ hooks }): writes, reads, peer batches | ❌ none — doc events cannot veto | ❌ none | ❌ subscribePreCommit sets message/timestamp only | ❌ none | ✅ pre/post insert/save/remove | ⚠️ transform-pouch (pouchdb-community) | ✅ middleware: willSet*/willDel* (v8+) | ✅ DBCore middleware, table hooks | ❌ none (storage gatewayInterceptor only) | ✅ Gun.on('opt') → put/in/out middleware | ⚠️ custom access controller canAppend | ❌ none | ⚠️ materializers; onPush on the backend | ⚠️ your API; Phoenix.Sync.Writer callbacks (Elixir) | ⚠️ your backend's upload handler; no client hook | ✅ custom mutators + handleMutateRequest handler | ❌ no write hooks; permissions only | ⚠️ no write hooks; rules reject, webhooks after commit |
| Observability | ✅ OpenTelemetry spans and metrics (injected), stats() | ❌ none | ⚠️ setLoggerFactory, doc-metrics events; no OTel | ❌ none (Inspector for debugging) | ✅ OpenTelemetry metrics (jazz.*); no spans | ⚠️ logger plugin (Premium) | ⚠️ pouchdb-debug logging only | ⚠️ getStats() counters; dev Inspector | ❌ none; syncState observable | ⚠️ structured logs (FP_DEBUG), tracer events | ⚠️ console.STAT, lib/stats.js counters | ❌ none | ⚠️ structured Console; no OpenTelemetry | ✅ OpenTelemetry built in; devtools | ✅ server: OTLP traces, Prometheus/StatsD, Sentry | ⚠️ service: Prometheus metrics, JSON logs; client logger83 | ✅ zero-cache: OTLP logs, traces, metrics84 | ⚠️ log levels, client.logs; Sentry on server | ⚠️ client verbose/logger; server OTel traces to Honeycomb |
| Typed errors | ✅ SyncomError with a code | ❌ plain Errors, no codes | ❌ plain errors, no codes | ❌ plain Error messages, no codes | ⚠️ load states unavailable/unauthorized/deleted | ✅ RxError.code (e.g. COL23) | ✅ status + name (e.g. 409 conflict) | ⚠️ tinybase:<n> in messages, undocumented | ✅ Dexie.*Error by name; policy codes | ⚠️ NotFoundError; otherwise plain Error | ❌ ack.err strings | ❌ plain Error messages | ✅ typed errors (Result, EvoluError) | ✅ Effect tagged errors (_tag) | ⚠️ error classes, FetchError.status; no codes | ⚠️ service PSYNC_* codes; client errors untyped | ✅ ErrorKind codes, ApplicationError, StorageFailureError | ⚠️ TriplitError subclasses: name + HTTP status | ⚠️ InstantAPIError body.type on HTTP; transact untyped |
| TypeScript types | ⚠️ hand-written .d.ts; record data unknown | ✅ shipped .d.ts (from JSDoc) | ✅ TS .d.ts; Doc<T> generics | ✅ TS .d.ts; typed containers | ✅ inferred from co.* schemas | ✅ TS; doc type inferred from schema | ⚠️ @types/pouchdb (DefinitelyTyped) | ✅ TS; schema-typed with-schemas | ✅ TS; EntityTable, DexieCloudTable | ✅ TS; generic document types | ✅ shipped .d.ts | ❌ none shipped (JSDoc only) | ✅ TS, inferred from schema | ✅ TS, inferred from Effect Schema | ✅ TS; ShapeStream<Row> generic, no codegen | ✅ shipped; row types inferred from Schema | ✅ TS, inferred from schema.ts | ✅ TS, inferred from schema (Entity, QueryResult) | ✅ inferred from i.schema; instant-cli pull writes it |
| UI bindings | ❌ none | ⚠️ editor bindings official; React via react-yjs (third-party) | ✅ @automerge/react, Svelte store, Solid primitives | ✅ loro-mirror-react; editor bindings | ✅ React, RN/Expo, Svelte; Vue (community-jazz-vue) | ✅ rxdb/plugins/react; Vue, Angular, Preact signals | ⚠️ third-party use-pouchdb | ✅ ui-react, ui-solid, ui-svelte | ✅ dexie-react-hooks; liveQuery observables | ✅ use-fireproof React hooks | ❌ none official | ❌ none | ✅ @evolu/react, @evolu/vue, @evolu/svelte | ✅ @livestore/react, solid, svelte | ✅ React useShape (@electric-sql/react) | ✅ @powersync/react, @powersync/vue, TanStack | ✅ @rocicorp/zero/react, /solid; Vue/Svelte community | ✅ @triplit/react, react-native, svelte, vue, angular, solid | ✅ React, React Native, Svelte, Vue, SolidJS packages |
✅ built in, or in an official package of the same project (named) · ⚠️ partial, opt-in, with a caveat, or through a third-party package (named) · ❌ not built in · 🔜 planned by the maintainers, no code yet · — does not apply. Every cell of the alternatives was decided by reading the source of the latest published package and its official docs in October 2026, not its README; the syncom column is this repository.
- npm
latestof@automerge/automerge-repoand its adapters points at an alpha (2.6.0-alpha.3); the last stable is 2.5.6 (2026-05). - Jazz 2.0 is a rewrite in alpha (2.0.0-alpha.59: tables, SQL-like queries) and jazz.tools/docs already documents it; the 0.20 docs ship in the package (
bin/docs-index.txt) and at classic.jazz.tools. - The addon peers on
dexie ^4.4.7, which is published only under thedevtag as of 2026-10-21. - No npm release since June 2024.
- npm lags the repository: master has commits through October 2026, but nothing has been published since July 2025.
- Effectively unmaintained: the last commit on main is from 2025-09-11, the co-founder joined Supabase on 2025-10-08 and Supabase said it would not integrate Triplit, and www.triplit.dev (docs and cloud) returns 410 Gone as of 2026-10. The repo is not archived, but no maintainer has answered a 2026 issue asking whether the project is maintained.
- Instant Cloud is closed to new signups and shuts down on 2027-08-31 after the team joined OpenAI; the code stays open source for self-hosting.
- The open-source core limits open collections across the process: the docs say 13; in the code (
src/rx-collection.ts:258-262) the 14th opens after a delay of about 1.8 s and the 15th throwsCOL23. RxServer (rxdb-server) is SSPL. - Free SaaS tier allows 3 production users; on-prem is a one-time licence from €3,495, source code only with Enterprise.
- package.json says AFL-2.0, the shipped LICENSE.md is Apache-2.0, and the repo README says MIT or Apache-2.0.
- The PowerSync Service (needed to sync) is source-available under the Functional Source License: competing commercial use is barred, and each version becomes Apache-2.0 two years after release; the SQLite core extension is Apache-2.0.
- The browser client itself is
AGPL-3.0-only, not just the server.@triplit/db,@triplit/serverand@triplit/server-coredeclare no license on npm, so the AGPL-3.0 LICENSE at the repo root applies to them. - Most are zero-cache's (fastify, postgres, @opentelemetry/sdk-node, @rocicorp/zero-sqlite3, ws, jose…); the client modules import only a few (e.g.
@rocicorp/lock,@rocicorp/logger,@badrap/valita). - Filesystem and SQLite storages are Premium; the free SQLite trial stops at 500 documents and after 500 SQL calls per storage instance (the 251st serial insert fails).
@electric-sql/clientalone keeps rows in memory; a persisted Node replica is PGlite (@electric-sql/pglite) fed by@electric-sql/pglite-sync.@instantdb/coreitself does nothing withoutwindow(no socket, no storage), so Node gets only the online admin SDK.- The default is
storage: 'memory': data only persists when you passindexeddb. There is no OPFS or browser SQLite option. - The official WebSocket server has no authentication or update validation; its sample upgrade handler only says 'You may check auth of request here'.
- The official sync server calls itself 'an unsecured Express app' — authentication is yours to add.
toCloud()defaults to dev endpoints (fireproof-v2-cloud-dev.jchris.workers.dev,dev.connect.fireproof.direct); the backend source sits in the repo'scloud/.- The default transport is
wss://free.evoluhq.com, a testing relay that admits anyone and may delete data. - A snapshot merge (bootstrap) and an op from a newer schema version (
ahead: 'apply', the default) are applied unvalidated, and a record outside any collection has no validator. - Runtime validation (0.20.10+) defaults to
warn: invalid local writes go through with a console warning, although the source comment saysstrict. - Documents are validated only when the storage is wrapped with a validator plugin.
- A cell that fails the schema is dropped or defaulted silently, and schematizers keep only type, enum and default.
- Mutations accept already-validated values: Evolu does not run the column validators on insert/update, nor on received changes.
- The client
Schemadoes not validate writes (mismatched types are silently CAST); validation is your backend's on upload, and a rejected write is rolled back at the next checkpoint. createSchema/table().columns()only type ZQL and declare relationships: the client'sinsert/updatestore values unchecked, and Postgres checks them when the server mutator commits.- Adding, removing or renaming a column invalidates every shape on that table, so clients get
409/must-refetch, drop their local copy and re-sync from scratch. Documents.get(key)also walks the oplog from the head, so reads cost O(log length).- Two clients can commit events that collide; a materializer that then fails rolls back that event, which the docs say will 'typically halt' the system.
add([x])/remove([x])merge across sync but return a sorted array, so element order is not kept.- A counter that one node resets while another increments it does not converge to the intuitive value; see Guarantees and limits in the Sync guide.
- A number in a
Y.Mapis a register: of two concurrent increments, one is lost. - Increments add up only if the server mutator applies them atomically (raw SQL
n = n + 1, orserializable+tx.retryOn); the default isolation is Postgresread committed. y-webrtcdefaults to the public signaling serverwss://y-webrtc-eu.fly.dev; pass your ownsignalinglist (and apassword) for anything private.- The docs say providers sync subdocuments, but
y-websocket,y-webrtcandy-indexeddbcontain no subdocument handling — you open one provider per subdocument. - The compressed document still carries the full change history — Automerge does not drop history.
- Compaction copies every block the clock reaches into the new CAR, so a newcomer still downloads the history.
- Docs still call compaction planned, but the code has it behind an undocumented
experimental_compactionshape param (default false), so by default a newcomer replays the snapshot plus every change since the shape was created. - Not wall-clock LWW: a concurrent
Y.Mapset is won by the writer with the higher randomclientID, whatever the times. - The winner is chosen by operation id (counter, then actor), not wall-clock time;
getConflictsreturns the losing values. compareTransactionsreturns 0 for equalmadeAtfrom different sessions, so a same-millisecond tie falls to arrival order.- A whole-object
put()overwrites concurrent property updates;where().modify()is re-run on the server. - Order comes from each writer's machine clock (
+new Date): a write stamped in the future is held in asetTimeoutuntil then and wins; equal states break by comparing JSON strings. - A change stamped more than 5 minutes ahead of the receiver's clock is quarantined until system time catches up.
- Merge-conflict handling isn't implemented yet (#253): concurrent events simply apply in the backend's order.
- The client never merges: it holds its checkpoint while uploads are pending, then adopts the server's checkpoint exactly; plain UPDATEs in the backend give per-field LWW by arrival order.
- No field-level merge: the server re-runs each mutator against current Postgres state, so concurrent writes to a column are last-processed-wins unless your mutator checks.
- The FAQ still describes per-attribute HLC timestamps from 0.x. In 1.x the server stamps each incoming batch with its own clock when it arrives, so an edit made offline hours ago overwrites newer values when it finally syncs.
- Order is server arrival, not a timestamp: an offline write replayed on reconnect overwrites newer writes made meanwhile.
- Ops written before any node was registered, and compacted since, cannot reach a host (
UNDELIVERABLE). - Reconnect re-sends only queued messages held in memory and live
gets; there is no persisted outbox, and src/websocket.js:5 still reads 'TODO: resync upon reconnect'. - Writes queue only while
connecting(disconnectTimeoutMs); docs-vs-code: the shipped client also accepts writes inneeds-auth, which the docs list as rejected. - A withheld op would leave a gap in its origin's stream, so a host that skips an op still relays it and every node re-checks it; a throwing
beforeApplyrefuses the whole page instead. - A write by a non-writer is stored and relayed, then ignored by every reader (
markInvalid); content checks need a server worker. middlewareon the server's own MergeableStore guards only that copy;WsServerstill relays the message to the other clients.- A rejected incoming entry is caught and only
console.error'd; the app gets no event for it. - Whoever can write the directory can rebuild the chain; a read checks a record against the block its file names, so a block planted beside the chain passes. The ops in the log are what is signed.
- The default
'verify'takes an unsigned op;sync.signatures: 'require'refuses it. A bootstrap re-signs the host's state under the joiner's key, andhellodoes not yet prove possession of the key. - Only data under a user's
~pubkeysoul is signed and verified; the public graph accepts unsigned writes from anyone. - The RSA key pair that unseals the records sits unprotected in
<path>/keys/, beside the data. - The browser keeps the account secret in
localStorageby default, so this protects data from the server, not from someone with the device. - The free plugin is AES-CBC in crypto-js's passphrase mode (MD5 key derivation, no MAC); an unsalted SHA-256 of the password is stored beside the data.
- The Loro team says not to rely on it for production-grade security yet.
- The encryption wrapper sits below
collection.storageInstance, which is what the replication reads. - Needs the encryption middleware moved above sync by hand; indexed fields are not encrypted.
MetaGateway.putattaches the ledger's key material to every meta update, so the sync service holds the key although the docs call sync end-to-end.- In 4.0.0 the
documentstype destructuresencryptinstead ofencryption, so document stores silently stay unencrypted (issue #1253, fix PR #1257 open). - Peer ids are self-asserted in the WebSocket join message, so access by peer id needs authentication at the socket.
- RxServer is SSPL and its defaults are open: with no
authHandlerevery request is admitted, and the Express adapter's CORS reflects any origin with credentials. - CouchDB has no per-document read rules; the usual pattern is a database per user.
- Anyone who knows a database address can replicate and read it unless it is encrypted.
- Electric serves any data its Postgres role can read to any caller;
ELECTRIC_SECRETmust be added by a proxy that also pins the shape's table andwhere. - A rule that is not set evaluates to true: everything is open until you write rules.
- The chain keeps an unsalted SHA-256 of every version of the record, which can confirm a guessed record.
- GC clears the document state only; update logs kept by providers or peers hold the payload until they compact (
y-indexeddbmerges after 500 updates). - A shallow snapshot only syncs with peers past its start version, and other replicas keep their full history.
- A removed document keeps its data in the tombstone until cleanup (default 31 days, after replication is in sync).
- Purges do not replicate; other adapters return an error.
deleteOwneranddeleteDatabasecalltodo(); rows can only be soft-deleted (isDeleted) and stay inevolu_history.- Without compaction a deleted row's earlier entries stay in the shape log on the Electric server (and in HTTP caches) and are replayed to new clients until the shape is deleted (
DELETE /v1/shape) or rotated. - No tracing and no OTLP push to your own collector, yet the self-hosted service pushes anonymous metrics over OTLP to pulse.journeyapps.com unless
telemetry.disable_telemetry_sharing: true. - zero-cache also sends anonymous usage telemetry to Rocicorp by default (
ZERO_ENABLE_TELEMETRY=falseorDO_NOT_TRACK=1to opt out).
Where each one fits
- syncom: records with a schema, queries and live views, kept on the device and replicated between servers, browsers and peers. Any node can host, so no service is needed, and the history is verifiable and can be purged. Zero dependencies.
- Yjs, Automerge, Loro: collaborative editing. Text, rich text, lists and trees merge character by character; storage, queries and access control are yours. Yjs has the largest ecosystem of editor bindings, Automerge keeps the full history, and Loro has the richest container types.
- Jazz: an app framework built on signed, encrypted CoValues, with groups and roles. It needs a sync server (Jazz Cloud or self-hosted).
- RxDB: a reactive JSON-document database with many storages and replication plugins. Disk storage on Node.js and several features are Premium.
- PouchDB: CouchDB's replication protocol in the browser, with a CouchDB as the hub.
- TinyBase: a small reactive in-memory store with many persisters and a simple sync protocol.
- Dexie Cloud: IndexedDB with a hosted (or licensed on-prem) sync service, realms and roles.
- Fireproof: an encrypted, content-addressed document ledger that syncs through its cloud.
- GUN, OrbitDB: peer-to-peer graphs and logs, over GUN's own mesh or libp2p and IPFS.
- Evolu: end-to-end encrypted SQLite on the device, synced through a blind relay.
- LiveStore: event sourcing into SQLite on the client, with a central sync backend.
- ElectricSQL, PowerSync, Zero: Postgres (or MongoDB, for PowerSync) is the source of truth. Clients get shapes, buckets or query results, and writes go through your API.
- Triplit, InstantDB: a sync database with its own server, query language and permissions.