# How sync works Source: https://selfstore.dev/docs/how-sync-works The clock and the merge, explained - why serverless convergence needs more than wall time, what a tombstone is for, and the properties the fuzz suite pins down. selfstore keeps several copies of the same data in agreement **without a server**. Each device edits its own copy offline; when the copies meet (through the shared backup file, not a sync service) every device folds the others in, and they all end up identical. No server means **no referee**. Two devices change the same record while offline, then meet: someone has to decide who wins, and every device must decide the *same* way, in any order. That is the whole problem this page explains. The everyday API view lives in [Multi-device sync](https://selfstore.dev/docs/sync); this is the inside of the machine. The engine is importable on its own as `selfstore/sync` if you want just the merge, no store. ## The clock You cannot order edits with `Date.now()`: device clocks disagree, and a phone running five minutes fast would win every conflict. A **Hybrid Logical Clock** is a timestamp built from three parts: ``` wall time | counter | device id ``` - **wall time** keeps it roughly in step with real time; - the **counter** breaks ties within the same millisecond, and keeps rising even if the wall clock jumps backwards, so a clock never goes down; - the **device id** makes the order *strict*: two devices can never produce the same stamp, so "later wins" is never a coin flip. It is encoded as a fixed-width string, so comparing two clocks is a plain string comparison, and string order equals edit order. Issuing one is a handful of lines: ```ts /** Issue a clock for a local event. Monotonic per node even if the wall clock moves back. */ export function issue(prev: Hlc | null, node: string, wallNow: number = Date.now()): Hlc { const p = prev ? decode(prev) : null; const prevWall = p ? p.wall : 0; const wall = Math.max(wallNow, prevWall); const counter = wall === prevWall && p ? p.counter + 1 : 0; return encode({ wall, counter, node }); } ``` When a device reads a clock from another replica it folds it into its own (`receive`), so its next stamp is guaranteed to sort after everything it has already seen. Causality survives even when the wall clocks lie. ## The merge Records are matched by their string `id`. On every local save the engine **stamps** what changed: a fresh clock for every added or modified record. Change is detected by content hash - nothing is injected into your objects, the metadata lives in a small sidecar that travels with the backup. A record that disappeared leaves a **tombstone**, a dated "this was removed". Deletion must be remembered: a device that never saw the delete would otherwise re-contribute the record on its next merge, and it would come back from the dead everywhere. At merge time, for each id, the later clock wins. A tombstone competes like any other write, so a delete beats an earlier edit and loses to a later one. What "wins" means is [configurable per collection](https://selfstore.dev/docs/sync): the whole record (`lww-set`), field by field (`lww-map`), append-only union (`grow-set`), a single value (`lww-register`), or handed to the app instead of resolved (`manual`). Tombstones accumulate - remembering deletions is the price of convergence. The [pull-model store](https://selfstore.dev/docs/advanced) can prune them past a horizon (`tombstoneHorizonMs`), safe as long as the horizon comfortably exceeds the longest a device ever stays offline. ## Why you can trust it "Converge" has a precise meaning, and the fuzz suite checks it on thousands of random, seeded edit histories: - **order does not matter**: merging A then B equals merging B then A; - **duplicates do not matter**: merging the same copy twice changes nothing; - **grouping does not matter**: a three-way merge lands in the same place however you pair it up. When a case fails it prints its seed, so the exact failing history replays as a fixed regression test forever after. ## What it is not - **Full-state, not delta**: every merge works on the whole dataset. Fine at the MB scale, wasteful for very large data. - **Not a sequence CRDT**: for live collaborative text, store a Yjs or Automerge document as a binary file in the snapshot and let the store carry and sync it. Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt