Local-first sync you can watch merge. A ~150-line, zero-dependency CRDT engine plus a live three-device playground: edit a shared list on any device, flip a device offline and keep editing, then reconnect and watch the changes reconcile — with no lost work and no fake winners.
Open index.html in any browser. No build step, no server, no tracking, works fully offline.
"Local-first" software keeps working offline and syncs when it can — but the moment two devices edit the same data while apart, you need a rule for merging that never loses a write and always agrees. SyncKit is the smallest honest version of that rule you can learn in one sitting:
- Hybrid Logical Clock (HLC). Every write gets a timestamp that is monotonic on each device and
globally comparable across devices, without synced wall clocks.
(wall, counter, node)gives a total order, so all replicas agree on "which write is newer." - Last-Write-Wins map CRDT. State is one HLC-stamped register per field.
merge()keeps the higher stamp per field. Because merge is commutative, associative, and idempotent, replicas that have seen the same writes converge — regardless of arrival order or how long a device was dark. - Tombstone deletes. A delete is a
__deletedLWW field, so "delete on A" vs "edit on B" merges deterministically instead of resurrecting the row.
The payoff you can see in the playground: two devices editing different fields of the same row both win; two devices editing the same field resolve to the later write, identically everywhere.
import { LWWMap } from './synckit.js';
const phone = new LWWMap('phone');
const laptop = new LWWMap('laptop');
phone.set('milk', 'text', 'Oat milk'); // edit offline on the phone
laptop.set('milk', 'done', true); // edit offline on the laptop
laptop.merge(phone.snapshot()); // reconnect → exchange state
phone.merge(laptop.snapshot());
// both replicas now agree: { id: 'milk', text: 'Oat milk', done: true }| Method | What it does |
|---|---|
new LWWMap(nodeId) |
A replica identified by a stable nodeId (used as the clock tiebreak). |
.set(rowId, field, value) |
Local write; stamps the field with a fresh HLC. |
.delete(rowId) |
Tombstones a row (a LWW delete). |
.get(rowId, field) |
Current value, or undefined. |
.rows() |
Live (non-deleted) rows as plain { id, ...fields } objects. |
.merge(remoteState) |
Fold another replica's snapshot() in; returns the number of fields changed. |
.snapshot() |
A JSON-safe copy to send over any transport (WebSocket, postMessage, a file). |
Transport is deliberately not included — SyncKit converges on whatever pipe you already have. Gossip snapshots on an interval, exchange them on reconnect, or diff-and-send; the merge is the same.
- LWW discards the loser on a same-field conflict. That's the right trade for profiles, settings, toggles, and titles — not for collaborative rich text (reach for a sequence CRDT there).
- Snapshots are whole-state. Fine to thousands of rows; add delta-sync for very large sets.
- HLC assumes device clocks are roughly right; it tolerates skew and backward jumps, but a device years in the future would dominate ordering until others catch up.
synckit.js— the entire engine (HLC + LWW-Map CRDT). Read it; it's short.index.html— the self-contained playground (imports the engine, no other dependencies).
MIT licensed. Have at it.