Search selfstore
v1.8.21

Multi-device sync without a sync server

Storage-as-mailbox, hybrid logical clocks and per-collection merge strategies - how deterministic convergence works when nobody runs a server.

Sync is the feature that pushes local apps onto servers. Durability alone never justifies the ops bill; “I want it on my phone too” does. So it is worth being precise about what a sync server actually contributes, and how much of it survives being replaced by a file.

Storage as a mailbox

A sync server does three jobs: it holds the truth, orders the writes and stays reachable. The local-first observation is that only the third job needs infrastructure, and users already have reachable storage: a Drive, a Nextcloud, a folder on a NAS.

So invert the design. Each device pushes its complete state as an encrypted file to shared dumb storage, pulls whatever is there, and merges locally. The storage orders nothing, resolves nothing, understands nothing (it holds ciphertext). All intelligence moves into the merge, which must now converge replicas that edited independently, offline, with lying clocks.

That merge is the actual engineering. This is how selfstore does it.

Clocks that survive lying wall clocks

“Last write wins” needs a defensible notion of “last”. Wall clocks drift and jump; pure logical clocks lose all human meaning. A hybrid logical clock (HLC) rides physical time when clocks agree and falls back to logical ordering when they do not, so “later” stays close to human intuition without ever going backwards or producing ties.

Every record carries an HLC stamp. Two replicas merging the same inputs reach the same result, in any order, from any starting point: convergence is deterministic, which is what lets the storage stay dumb. selfstore fuzz-tests this with seeded randomness: two-way merges are symmetric and idempotent, and set strategies are order-independent across replicas within their documented contracts.

One strategy does not fit all collections

The right conflict semantics depend on what the data means, so they are chosen per collection:

Losing data loudly, never silently

Any last-writer-wins system drops the losing side of a true concurrent edit; pretending otherwise is marketing. What a merge owes its users is visibility: every selfstore converge that changed anything is journaled, and same-record conflicts carry both values, so the app can show “your phone’s version of this note was replaced, here it is” and offer a restore.

Deletions need memory too: a delete is a tombstone, kept so a device that was offline for a month does not resurrect the record. Tombstones grow with total deletions, so pruning exists, opt-in, with the trade-off documented: compact tombstones older than your horizon only if every device syncs more often than that, or a long-offline device will resurrect what it never saw deleted.

Where the honesty line sits

Field-level LWW is not a CRDT for text. Two people typing in the same paragraph want operational merging (Yjs and Automerge are excellent), and a CRDT document embeds happily inside a selfstore snapshot as a binary file: the two compose rather than compete. Likewise, whole-state sync (download, merge, re-upload the full backup) is proudly O(state): perfect at the MB scale of personal apps, wrong for multi-GB datasets on metered connections. The sync guide states the limits in one place.

What you get inside those limits: convergence for one person’s devices, and read-write sharing between a few people over crossed read-only links, with no server anywhere, no accounts, and nothing in the loop that can read the data. The mailbox does not need to be smart. The merge already is.


Try selfstore in five minutes, or read the other notes.