Edge sync without a server
Several devices edit the same work order with no link between them. When they meet, the edits merge to the same result on every one, and no server decides who wrote last. In the SDK, on by default, on storage you already own.
Diagram: two handhelds edit the same work-order document while offline. unit-07 sets the status and adds a visit, unit-12 records a reading and an assignee. When the mesh link returns, compact deltas cross and both devices hold one merged document with no conflicts.
Two handhelds edit the same work order with no link between them, then meet and hold one result.
Why shared state needs its own primitive
A message is not a value. A message says something happened. A checklist or a stock count keeps changing, on more than one device. Send those changes as messages and you are writing merge rules by hand, one per field, and finding the bugs the first time two people edit at once.
Merging is a property of the type. A map, a list, a text, and a counter each know how to combine two offline edits. Two devices write, meet over any transport, exchange only what changed, and land on the same document. No server picks a winner.
Your cloud stays the system of record. The document is where work happens on the site. When a link returns, your app reconciles it with the database you already run. Nothing here replaces it, and nothing here needs a server to exist.
Four types, four merge rules
Pick the shape that matches how the data is edited, and the merge comes with it. Two devices edit offline, and this is what every replica holds after they meet.
Different keys both survive. The same key converges on one value, the same one everywhere.
status"in progress"assigneeunchangedstatusunchangedassignee"unit-12"status"in progress"assignee"unit-12"Both appends survive, in an order the type decides, not whoever reconnected first.
readings[58.9, 61.4]readings[58.9, 62.0]readings[58.9, 61.4, 62.0]Inserts land in their places. Neither device's words overwrite the other's.
note"Urgent: replace valve"note"replace valve before shift"note"Urgent: replace valve before shift"Increments add up. A last-writer-wins value would have lost one of them.
visits0 +1visits0 +2visits3The life of an edit
From a tap on one device to the same value on another. Each stage has a guarantee attached, and none of them involves a server.
- EditA write lands in the document and is batched with the writes around it.
- DurableThe batch reaches storage, sealed under a per-install key, on the backend you chose.
- Event
data_changedfires only now, so a screen that re-renders on it shows state that survives a restart. - DeltaOnly the change travels, as MLS ciphertext inside the space, up to 32 KiB per frame.
- ConvergeThe peer applies it. Duplicates and out-of-order frames are absorbed, and anti-entropy fills any gap.
In code
A space is a peer address or a group id. Write to a document, listen for the durable change, and flush when you need a guarantee.
import { DataStore } from '@offline-protocol/mesh-sdk';
const store = new DataStore();
// a 1:1 space is named by the peer's address
await store.mapSet(peerAddress, 'work-order', 'fields', 'status', { kind: 'text', value: 'in progress' });
await store.listPush(peerAddress, 'work-order', 'readings', { kind: 'float', value: 61.4 });
await store.counterIncrement(peerAddress, 'work-order', 'visits', 1);
// fires after the change is durable, never before
protocol.on('data_changed', (event) =>
render(await store.docJson(event.space_id, event.doc_id)));
// when the app must know a change survived a crash
await store.flush(peerAddress, 'work-order'); The same operations exist on iOS and Android natively, in Python, and on the Rust engine. Two runnable examples in the repository show a store reopening its records after a rebuild, and two replicas editing the same document offline and converging.
Under the hood
The mechanics beneath the API, each verified against the SDK, and the lines this version does not cross.
How replication works
- Spaces are MLS scopes
- A 1:1 session is a space named by the peer address; a group is a space named by the group id. The roster that already exists is the membership, so there are never two rosters that disagree about who is in the room.
- Compact deltas
- Only the changes travel. Duplicate and out-of-order deltas are absorbed, so reconnection can be partial, brief, or repeated. A sync frame carries up to 32 KiB of document bytes; larger catch-ups ride the media path.
- Anti-entropy
- Replicas exchange version offers when a session confirms and periodically after that, then send only what the other side is missing. Every leg ends; offers marked as replies are never answered with offers.
- Durability before events
- Edits batch before they reach storage. The
data_changedevent fires after the change is durable, never before. Callflushwhen your app must know a change survived a crash. - Bounded size
- A document is capped at 1 MiB compacted, with a warning event at 768 KiB so the cap is never met for the first time as a failed write. History compacts when the delta log passes four times the document or 1,024 commits.
- Contained imports
- Remote document bytes are only ever handed to the engine from a sealed record whose AEAD tag verified, and are bounded, inspected, and parked if they cannot apply. A malformed import from a peer cannot take the app down.
- Attachments by reference
- A value can be an attachment: a SHA-256 hash, size, name, and MIME type. The reference replicates like any value; the bytes are fetched pairwise on request and verified against the hash on arrival. Your app owns where bytes are stored.
- Swappable storage
- The default backend persists in the app container, sealed under a per-install key. A custom backend is one line to install and must pass the conformance suite, which catches adapters that return success and still lose overwrites.
What this version does not do
- No queries
- No query language, no index, and no partial replication. A space replicates whole, so model state a member is entitled to hold in full.
- No hosted component
- Relays and gateways carry sync frames as ciphertext they cannot read. Nothing in the data layer requires a server.
- No deletion tombstones
- Deleting a document locally does not delete it on peers. Empty it instead, since edits inside a document replicate.
- No attachment bytes in groups
- References replicate everywhere; blob fetches are pairwise only.
- No blob garbage collection
- Deciding when stored bytes are unreferenced is your app's job. Each of these is a recorded decision, with the reasoning in the design record and the architecture decision records in the repository.
Where it applies
- Plant floor and field crews
- A work order, a checklist, or a shift log edited by several people on a site with no backhaul converges when the devices meet, and reconciles with the system of record when a link returns.
- Fleets and yards
- Assignments and counts shared across vehicles and handhelds stay consistent through dead zones without a server deciding who wrote last.
- What it builds on
- Sync frames travel over the DORS mesh inside MLS sessions, with the reliability layer underneath. Compare the shape against a database product on the Ditto page.
Edge sync FAQ
What is a replicated document?
A document is offline-first shared state: any member of a space can edit it while disconnected, and when replicas meet again the edits merge deterministically. Every replica reaches the same result in any order, with no server arbitrating. Messaging is synced events; documents are synced state.
What kinds of data can a document hold?
Four collection types: map, list, text, and counter. Values are scalars (text, integers, floats, booleans, bytes) or attachment references. Structured values go in as JSON strings and merge whole.
Do I need a new database?
No. Documents persist on-device by default, sealed under a per-install key, and the storage backend is a swappable adapter with a conformance suite. SQLite adapters for Swift, Kotlin, and Python ship as examples. Your existing database and your cloud remain the system of record.
Is a document a database with queries?
No. A document is replicated state, not a query engine: there is no query language and no index, and your database stays the system of record. Since v0.27.0 a device can replicate part of a space with setInterest, remove a document from every replica, and fetch attachment bytes from a group member under the group key.
Who can see a document?
Membership is the MLS roster. A 1:1 session or a group is the space, so there is no second membership system to keep in step. Sync frames travel as MLS ciphertext, and relays and gateways cannot read them.
Is there a hosted component?
No. Relays and gateways carry sync frames as opaque ciphertext, and nothing in the data layer requires a server. Replication is peer to peer over whatever transport is available.
Which SDK version shipped this?
Replicated documents shipped in SDK v0.23.0 and are enabled by default; the sealed engine seam and the no_std work landed in v0.24.0, and v0.27.0 added partial replication, removal from every replica, and attachment bytes inside a group. The engine underneath is Loro, pinned exactly, and it is named nowhere in the public API so it can be replaced without a breaking release.

