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.

4collection types
32 KiBper sync frame
1 MiBper document, compacted
v0.23SDK release that shipped it

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.

unit-07edited offline
unit-12edited offline
Every replicaafter they meet
MapKey to value

Different keys both survive. The same key converges on one value, the same one everywhere.

status"in progress"assigneeunchanged
statusunchangedassignee"unit-12"
status"in progress"assignee"unit-12"
ListOrdered

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]
TextCharacter level

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"
CounterSums

Increments add up. A last-writer-wins value would have lost one of them.

visits0 +1
visits0 +2
visits3

The 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.

On unit-07Over the meshOn unit-12
  1. EditA write lands in the document and is batched with the writes around it.
  2. DurableThe batch reaches storage, sealed under a per-install key, on the backend you chose.
  3. Eventdata_changed fires only now, so a screen that re-renders on it shows state that survives a restart.
  4. DeltaOnly the change travels, as MLS ciphertext inside the space, up to 32 KiB per frame.
  5. 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.

TypeScript
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_changed event fires after the change is durable, never before. Call flush when 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.

From the blog
Offline Protocol vs Ditto

A coordination layer with replicated documents next to an offline-first database.

Read more →
Mesh networking

DORS, reliable delivery, and file transfer under every sync frame.

Read more →

Shared state that converges without a server. Map, list, text, counter. Your storage.

Book an evaluation Read the docs