CRDT sync · Cloudflare Durable Objects

Real-time document sync that hibernates when idle

Subduction runs its peer-to-peer sync layer as a Cloudflare Durable Object — one object per document, each with its own SQLite. Clients exchange signed commits over a single WebSocket; the object evicts itself between bursts of activity and rehydrates from storage on the next message. You pay for work, not for waiting.

WebSocket endpoint
wss://subduct.io/sync/<document-id>

Every client that names the same <document-id> is routed to the same Durable Object — that is the whole coordination primitive.

Built for the edge

The sync engine is Rust compiled to WebAssembly, running inside the Durable Object next to its storage.

🗂️

One object per document

The document id maps to a stable Durable Object id, so load and storage spread across the fleet instead of a single hot instance.

💤

WebSocket hibernation

Sockets stay open while the isolate is evicted from memory. Identity and connections rebuild from persisted state on the next frame.

🔏

Signed handshake

Every peer proves an Ed25519 identity through a challenge/response handshake before it can read or write a document.

🛡️

Replay-safe across sleep

Accepted handshake nonces are persisted to SQLite, so a captured challenge can't be replayed after the object hibernates.

🧹

Alarm-driven compaction

An alarm fires only when there's work — expiring nonces and compacting redundant commits once they're covered by a fragment.

💾

Durable by default

Commits, fragments, and subscriptions live in the object's SQLite, so nothing is lost across evictions or restarts.

Connect in four steps

Point a Subduction client at the endpoint, complete the handshake, and subscribe to a document.

  1. Open a WebSocket to /sync/<document-id> — the worker routes it to that document's object.
  2. Discover & handshake using the service name subduction-do; both sides derive the same audience from it.
  3. Subscribe to the document to receive server-pushed commits as other peers write them.
  4. Sync — pull existing history and push your commits; the object fans them out to subscribers inline.

Open demo: this deployment runs a permissive policy — anyone who knows a document id can read and write it, storage is uncapped, and data may be reset at any time. It's for evaluation only; don't put anything private or durable here. Self-host with your own auth policy for real use.

// Browser client, via the subduction_wasm bundle.
import init, { Subduction, MemorySigner, MemoryStorage, SedimentreeId }
  from "./pkg/subduction_wasm.js";

await init();

const sync = new Subduction({
  signer: MemorySigner.generate(),
  storage: new MemoryStorage(),
  onRemoteHeads: () => render(),
});

const doc = SedimentreeId.fromBytes(docIdBytes);
const url = new URL(`wss://${host}/sync/${docIdHex}`);

// Handshake + identify the server peer.
await sync.connectDiscover(url, "subduction-do");
await sync.syncWithAllPeers(doc, true, 5000);

What happens on a message

The lifecycle is designed so no work is lost when the isolate is evicted.

route
/sync/<doc> is upgraded and forwarded to id_from_name(doc) — a dedicated, individually-hibernatable object with its own SQLite.
accept
The server socket is registered as hibernatable. No long-lived receive loop is held open, so the isolate is free to sleep between frames.
auth
The first frame is a signed challenge. The object verifies it, rejects replayed nonces from its durable table, and pins the peer identity to the socket.
sync
Subsequent frames carry commits and fragments. They're persisted, then fanned out to subscribers inline — before the handler returns and the object can hibernate.
alarm
When a fragment lands or a nonce is recorded, an alarm is armed. It expires stale nonces and compacts commits made redundant by fragments — then only re-arms if there's still work.