Skip to content
Alexis-TechnologiesPublic

About

Fast, zero-dependency distributed data sync engine 🌐

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

syncom

syncom

Fast, zero-dependency distributed data sync engine 🌐

npm CI node dependencies docs license

syncom is a local-first data engine for JavaScript. It stores records on disk — or in the browser's origin-private filesystem — commits every write to a hash-linked log so tampering is detectable, and replicates every write to peers as an operation that converges without coordination: last-writer-wins registers, per-origin counters, observed-remove sets, tombstones. The transport is injected — wrpc out of the box — and the package has no runtime dependencies.

'use strict';

const { open } = require('@alexify/syncom');

const main = async () => {
  const storage = await open({ path: 'data' });

  const id = await storage.insert({ name: 'Ada' });
  const record = await storage.record(id);

  record.on('update', (data, delta) => console.log('changed', delta));

  record.title = 'Ada Lovelace';
  await record.save();
  await storage.inc(id, 'hits', 1); // a counter: increments add up across nodes

  console.log(await storage.validate({ last: 10 })); // true
};

main();

📚 Documentation · Getting started · API reference

Table of Contents

Installation

Requires Node.js ≥ 22, or a modern browser with OPFS and Web Crypto.

npm install @alexify/syncom

Bundle size

Measured by pnpm size on every CI run. The browser entry carries a budget that fails the build when exceeded.

Entry raw min min+gzip budget
main entry — browser (@alexify/syncom) 296.0 KB 122.3 KB 41.9 KB 42.0 KB
main entry — node (@alexify/syncom) 279.9 KB 121.3 KB 41.6 KB —
wrpc adapter — browser (@alexify/syncom/wrpc) 3.4 KB 1.4 KB 0.7 KB 2.0 KB
contracts — browser (@alexify/syncom/contracts) 3.7 KB 1.6 KB 0.8 KB 1.0 KB
idb backend — browser (@alexify/syncom/idb) 3.7 KB 1.7 KB 0.8 KB 2.0 KB
search — browser (@alexify/syncom/search) 4.2 KB 1.7 KB 0.9 KB 2.0 KB

Quick Start

const { open, diff, merge } = require('@alexify/syncom');

// A schema turns entity names into collections.
const storage = await open({
  path: 'data',
  schema: { entities: { Post: {}, Author: {} } },
});

const author = await storage.Author.insert({ name: 'Ada' });
const post = await storage.Post.insert({ title: 'Draft', author: author.id });

// Records expose live properties and track their own delta.
post.title = 'Published';
console.log(post.delta());  // { title: 'Published' }
await post.save();

// Compare-and-swap, for writers that must not clobber each other.
await storage.swap(post.id, { title: 'Final' }, { title: 'Published' });

// The CRDT helpers work standalone, on plain objects. Numbers ADD, which is
// what lets two peers each increment a counter and converge.
merge({ hits: 10 }, { hits: 1 });              // { hits: 11 }
diff({ tags: ['a'] }, { tags: ['a', 'b'] });   // { tags: ['b'] }

Syncing between nodes

Host one end on a wrpc server, dial it from the other. wrpc is yours to install — syncom only takes the client you built:

// the hub
const { Server, defineRouter } = require('@alexify/wrpc');
const router = defineRouter({ syncom: storage.sync.unit({ access: 'public' }) });
await new Server({ router, host: '0.0.0.0', port: 8000, protocol: 'http' }).listen();

// a node — a server, a CLI, a browser tab
const { WrpcClient } = require('@alexify/wrpc');
const { createWrpcLink } = require('@alexify/syncom/wrpc');
const storage = await open({
  path: 'data',
  sync: { connect: async (node) => createWrpcLink(await WrpcClient.connect(node.url)) },
});
await storage.sync.addNode({ url: 'ws://hub.example.com:8000/api' });

Every node can be both, and ops relay hop by hop, so any connected graph of nodes converges.

Features

Area What you get
Storage Records with live properties and per-record deltas, schema-driven collections, compare-and-swap writes, counters and sets, encryption at rest (AES-256-GCM under an RSA-wrapped key), purge() for the right to be forgotten
Schema A metaschema model, any Standard Schema (zod, valibot, arktype), ajv or a compiled JSON Schema validates every write before it is stamped — locally and again on the hub. Versions ride on ops; an older record reads through a lens (migrations, then the validator's aliases and defaults); migrate() rewrites it. See Schema and validation
Queries select({ orgId, status: { $in } }).order('name').limit(20).fetch() over an in-memory record catalog: indexed queries with no I/O, unique keys, watch() for live views. See Queries
Full-text search select({ $text: 'quick fox' }): fuzzy, typo-tolerant, TF-IDF ranked trigram search over an entity's search fields, best first, narrowed by any equality — the index is @alexify/syncom/search (under 1 KB), injected at open({ search }). See Full-text search
Backends node:fs, OPFS, IndexedDB (@alexify/syncom/idb) or memory — five functions injected at open({ fsi }); durable: true for fsync, one instance per directory. See Backends
Replication Every write is an op in a persisted log; peers exchange ops over an injected link, resume from where they stopped, and bootstrap from a streamed snapshot when they cannot; scopes for partial replication
Convergence Last-writer-wins registers, per-origin counters, observed-remove sets, tombstones with revival — any order, any number of times, same result
Transport @alexify/syncom/wrpc adapts a WrpcClient — over a WebSocket, or a WebRTC PeerLink's remote from @alexify/wrpc/webrtc; storage.sync.unit() is the router unit a wrpc server, or a browser's PeerHost, mounts. Reconnects, resumable subscriptions, binary streams and rooms come from wrpc. See WebRTC
Integrity Every write appends a hash-linked block (chain: false turns it off for a hub); reads re-verify the record against the block that committed it and throw on a mismatch; repair(id) rebuilds a record from the log
Signed ops Every op is signed with the node's Ed25519 key, and the node id is that key: a peer verifies an op against its origin through any relay, a purge leaves a header that still verifies, sync.signatures: 'require' refuses unsigned ops. See Signatures
CRDT diff / merge / apply over plain objects — nested maps merge recursively, arrays are grow-only sets, numbers add so counters converge
Contracts @alexify/syncom/contracts: store a procedure in the chain by hash, load it anywhere, run it against a read-only view of storage; failures are recorded as blocks. A subpath, so the package root never runs a block's code
Portability One API over node:fs + node:crypto on the server and OPFS + Web Crypto in the browser, chosen by the browser export condition
Hooks and events open({ hooks }) runs your code inside a write, a read or a peer's batch — validate, stamp, redact, skip — and rejects with a typed HookError when it throws; on() events (change, apply, peer, sync, …) observe everything and can never break it. See Hooks and events
Observability Injected OpenTelemetry (open({ telemetry })): spans for local writes, applied batches, pushes, bootstraps and log rolls, plus outcome and lag metrics — nested under wrpc's spans, which carry the trace across the wire. See OpenTelemetry
Scaling Hubs that dial each other, scopes as buckets, per-peer push limits, a reserved shared-log contract for the broker-backed hub. See Scaling
Dependencies None. wrpc, metaschema and fake-indexeddb are devDependencies for the integration tests; everything is injected at runtime

Compared with alternatives

syncom sits where the CRDT libraries (Yjs, Automerge, Loro, Jazz), the local-first databases (RxDB, PouchDB, TinyBase, Dexie Cloud, Fireproof), the peer-to-peer and event-sourced stores (GUN, OrbitDB, Evolu, LiveStore) and the sync services (ElectricSQL, PowerSync, Zero, Triplit, InstantDB) meet. The comparison puts all nineteen in one table of forty features, each cell decided from the published source. In short: syncom is a database that merges field by field, not a text CRDT. It syncs with no service to run, its hosts validate and scope what peers send, and its log is verifiable and purgeable. It has no text or list CRDT and no UI bindings, and it is a pre-release, not on npm yet. RxDB, the closest of them, is compared feature by feature in 300 rows, from its code and every page of its docs.

Benchmarks

Measured against Yjs, Automerge, Loro, TinyBase, PouchDB, RxDB, Fireproof, GUN and OrbitDB on the same scenarios: insert, update, read, a converging counter, a batch, replicating a store, in memory and on disk. Each library runs in a fresh process, is used the way its documentation shows, and is checked to give the right answer before it is timed. The method and the interactive charts are on the Benchmarks page.

Replicate a store, in memory: An empty replica pulls a store of 1,000 records, in process, through the library's own sync protocol, until every record reads back. Nothing touches the disk. records/s, log scale, higher is better. Not shown: Fireproof (syncs through a cloud or self-hosted gateway only); GUN (needs its storage adapter to answer in Node.js).

Insert a record, on disk: Add one record to a store of 1,000. Each library's own persistence on node:fs, every write awaited. ops/s, log scale, higher is better. Not shown: RxDB (Node.js disk storages are Premium).

10 more scenarios Insert a record, in memory: Add one record to a store of 1,000. Nothing touches the disk. ops/s, log scale, higher is better. Not shown: GUN (needs its storage adapter to answer in Node.js). Update a field, in memory: Change one field of an existing record. Nothing touches the disk. ops/s, log scale, higher is better. Not shown: GUN (needs its storage adapter to answer in Node.js). Update a field, on disk: Change one field of an existing record. Each library's own persistence on node:fs, every write awaited. ops/s, log scale, higher is better. Not shown: RxDB (Node.js disk storages are Premium). Read a record, in memory: Read one record by id, as a plain object. Nothing touches the disk. ops/s, log scale, higher is better. Not shown: GUN (needs its storage adapter to answer in Node.js). Read a record, on disk: Read one record by id, as a plain object. Each library's own persistence on node:fs, every write awaited. ops/s, log scale, higher is better. Not shown: RxDB (Node.js disk storages are Premium). Increment a counter, in memory: Add 1 to a counter that converges when replicas increment it concurrently. Nothing touches the disk. ops/s, log scale, higher is better. Not shown: Yjs (no counter that converges); TinyBase (no counter that converges); PouchDB (no counter that converges); RxDB (no counter that converges); Fireproof (no counter that converges); GUN (needs its storage adapter to answer in Node.js); OrbitDB (no counter that converges). Increment a counter, on disk: Add 1 to a counter that converges when replicas increment it concurrently. Each library's own persistence on node:fs, every write awaited. ops/s, log scale, higher is better. Not shown: Yjs (no counter that converges); TinyBase (no counter that converges); PouchDB (no counter that converges); RxDB (Node.js disk storages are Premium); Fireproof (no counter that converges); GUN (no counter that converges); OrbitDB (no counter that converges). Insert 1,000 at once, in memory: Add 1,000 records in one call or transaction. Nothing touches the disk. records/s, log scale, higher is better. Not shown: GUN (needs its storage adapter to answer in Node.js). Insert 1,000 at once, on disk: Add 1,000 records in one call or transaction. Each library's own persistence on node:fs, every write awaited. records/s, log scale, higher is better. Not shown: RxDB (Node.js disk storages are Premium); GUN (failed: no acknowledgement in 30000 ms). Replicate a store, on disk: An empty replica pulls a store of 1,000 records, in process, through the library's own sync protocol, until every record reads back. Each library's own persistence on node:fs, every write awaited. records/s, log scale, higher is better. Not shown: RxDB (Node.js disk storages are Premium); Fireproof (syncs through a cloud or self-hosted gateway only); GUN (syncs over its WebSocket/WebRTC mesh only).

In memory

Library Insert a record
ops/s
Update a field
ops/s
Read a record
ops/s
Increment a counter
ops/s
Insert 1,000 at once
records/s
Replicate a store
records/s
syncom
default, createMemoryFsi() · v0.0.1
16,200 17,100 972,000 16,700 16,000 25,200
syncom
chain: false, createMemoryFsi() · v0.0.1
17,500 18,800 929,000 18,600 17,200 26,200
Yjs
Y.Doc, a Y.Map per record · v13.6.33
235,000 382,000 3,680,000 — 275,000 99,700
Automerge
Automerge.change, Counter · v3.5.0
512 1,990 10,600,000 1,950 8,960 20,900
Loro
LoroDoc, a LoroMap per record · v1.16.4
31,300 412,000 313,000 349,000 83,100 105,000
TinyBase
MergeableStore · v10.0.1
22,900 108,000 1,460,000 — 23,700 11,500
PouchDB
pouchdb-adapter-memory · v9.0.0
20,300 8,310 30,100 — 74,000 17,800
RxDB
memory storage · v17.6.0
16,700 50,400 281,000 — 2,220,000 64,100
Fireproof
memory:// store · v0.24.19
310 377 726 — 22,300 —
OrbitDB
documents, memory blockstore · v4.0.0
1,080 31.1 81.4 — 1,090 220

On disk

Library Insert a record
ops/s
Update a field
ops/s
Read a record
ops/s
Increment a counter
ops/s
Insert 1,000 at once
records/s
Replicate a store
records/s
syncom
default, node:fs · v0.0.1
1,520 1,340 958,000 1,480 1,690 5,830
syncom
chain: false, node:fs · v0.0.1
2,060 3,080 951,000 3,060 2,740 5,820
Yjs
y-leveldb · v13.6.33
19,200 22,400 3,690,000 — 155,000 53,700
Automerge
automerge-repo, NodeFS storage · v3.5.0
490 1,250 10,500,000 1,200 7,820 3,100
Loro
updates appended to a file · v1.16.4
19,500 43,300 285,000 42,600 66,900 108,000
TinyBase
MergeableStore, file persister · v10.0.1
172 304 1,390,000 — 15,400 11,100
PouchDB
LevelDB (Node default) · v9.0.0
26,400 8,810 42,000 — 65,500 16,500
Fireproof
file:// store · v0.24.19
300 335 720 — 20,700 —
GUN
RAD on node:fs · v0.2020.1241
3.63 3.77 749 — failed —
OrbitDB
documents, LevelDB · v4.0.0
952 17.4 82.4 — 921 193

Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 1 s samples (5 rounds for a batch or a replication), each library in a fresh process · 2026-10-22. Medians, rounded to three significant figures; — where the library has no such operation, no such storage, or no in-process sync, failed where it did not finish twice (the chart above says why).

Cold start

Cold start: Load the library, open a store of 1,000 records on disk, read one: a fresh process, median of 10 runs. Lower is better.

Bundle size

Bundle size: What a browser app ships to keep records on the device and sync them (esbuild, minified, tree-shaken, the network client left out), min+gzip, WebAssembly included. Lower is better.

Library What is bundled min min+gzip
GUN
gun@0.2020.1241
gun (localStorage; its WebSocket mesh is built in) 42.2 KB 17.1 KB
TinyBase
tinybase@10.0.1
MergeableStore + IndexedDB persister + custom synchronizer 56.4 KB 23.8 KB
Yjs
yjs@13.6.33, y-indexeddb@9.0.12, y-protocols@1.0.7
yjs + y-indexeddb + y-protocols/sync 80.2 KB 24.5 KB
PouchDB
pouchdb-browser@9.0.0
pouchdb-browser 117.8 KB 40.4 KB pouchdb-browser carries its HTTP adapter (the CouchDB client) with it
syncom
@alexify/syncom@0.0.1
open() over OPFS + the wrpc link adapter 124.1 KB 42.6 KB storage, schema, queries, the op log and the sync protocol are one entry
RxDB
rxdb@17.6.0, dexie@4.4.6, rxjs@7.8.2
rxdb + Dexie storage + replication 282.3 KB 90.1 KB
Fireproof
@fireproof/core@0.24.19
@fireproof/core 784.2 KB 204.0 KB
OrbitDB
@orbitdb/core@4.0.0, helia@6.1.4, libp2p@3.5.1, @libp2p/gossipsub@15.0.23, blockstore-idb@4.0.1
@orbitdb/core + helia + libp2p, IndexedDB blockstore 1185.6 KB 353.7 KB createHelia brings Helia's default libp2p configuration with it
Loro
loro-crdt@1.16.4
loro-crdt (no persistence of its own) 3405.0 KB
WebAssembly 3319.1 KB
1114.8 KB
Automerge
@automerge/automerge@3.5.0, @automerge/automerge-repo@2.6.0-alpha.3, @automerge/automerge-repo-storage-indexeddb@2.6.0-alpha.3
automerge-repo + IndexedDB storage 3744.5 KB
WebAssembly 3558.8 KB
1171.9 KB

syncom's own hot paths

Storage: syncom's own benchmarks: The public API end to end on node:fs: a write is an op appended to the log, a record file and a block. ops/s, log scale, higher is better.

Browser usage

The browser export condition selects browser.js automatically — no bundler configuration needed.

import { open, diff, merge } from '@alexify/syncom';

Crypto is asynchronous there (calculateHash, encrypt and decrypt return promises), and storage lives in the origin-private filesystem — or in IndexedDB through open({ fsi: createIdbFsi() }) from @alexify/syncom/idb. browser.d.ts declares those signatures separately. A browser node dials the mesh through @alexify/syncom/wrpc like any other, and hosts for its peers over WebRTC. See Browser.

Status

0.0.1 — a pre-release, not published to npm, and there is no release until the package does what it is meant to; the API may still change. Everything on this page works today and is covered by the test suite, including replication over real wrpc WebSockets. Known limits — a counter straddling a concurrent reset, a field written as two kinds at once, encrypted records staying local, no proof of key possession at hello — are listed under Guarantees and limits.

Documentation

Contributing

See CONTRIBUTING.md.

pnpm install
pnpm test              # node --test, recursive over tests/
pnpm test:coverage     # c8 over src/
pnpm test:types        # tsd against the .d.ts files
pnpm lint              # oxlint
pnpm format            # oxfmt (format:check in CI)
pnpm size              # bundle-size report; the browser budget fails the run
pnpm bench             # every script in bench/ (--save writes bench/results/engine.json)
pnpm bench:compare:install && pnpm bench:compare   # against the alternatives
pnpm bench:report      # the charts and the tables of the docs and this README
pnpm docs:dev          # the VitePress site

Changelog

See CHANGELOG.md.

Security

See SECURITY.md. Report vulnerabilities privately — do not open a public issue.

License

MIT © Alexis Technologies

About

Fast, zero-dependency distributed data sync engine 🌐

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages