Wire protocol
The protocol between client and server is a small set of typed events. This page is for sync-engine developers who want to compare notes; application code never touches these directly.
All events travel over an event bus — WebSocket frames in production, function calls in-process. The vocabulary is identical either way.
Client → server
Section titled “Client → server”| Event | Meaning |
|---|---|
doc:subscribe | Start receiving changes for a document. Server replies with doc:init (or doc:notfound). |
doc:unsubscribe | Stop receiving changes. |
doc:create | Create a document — id, type, initial data, optionally with embedded Yjs documents. |
doc:update | Change a document — a JSON Patch and/or Yjs updates, an optional guard (sequence or patch), and the client’s event id for optimistic confirmation. |
doc:delete | Soft-delete a document — it leaves the index and the read path, but its history is retained for restore. Takes an optional sequence guard (“delete only if unchanged since I looked”). |
doc:restore | Undo a soft delete — the document re-enters the index at the exact sequence it left, and subscribers receive a fresh doc:init. |
doc:get-events | Fetch a document’s event log, optionally bounded by sequence range. |
Server → client
Section titled “Server → client”| Event | Meaning |
|---|---|
doc:init | Full snapshot at subscribe time — sequence, type, data, and the state of embedded Yjs documents. |
doc:patch | An incremental change — patch and/or Yjs updates, the new sequence, and the originating client event id (so the originator can retire its optimistic entry). |
doc:error | A rejected event — categorized (validation failure, failed guard, limit), tied back to the client event id. Guard failures are expected signals, not faults. |
doc:notfound | The subscribed document doesn’t exist. |
doc:deleted | The document was soft-deleted — pushed to live subscribers when a delete commits, and the answer to subscribing to an already-deleted document (deliberately distinct from doc:notfound). |
doc:resume | The connection was re-established; client and server resynchronize subscriptions. |
Design notes
Section titled “Design notes”- Deltas dominate. After
doc:init, a subscriber only ever receives patches — full snapshots are never re-broadcast. - One update, two payloads. A single
doc:updatecarries structured patches and Yjs binary updates together, so a mixed edit (retitle + type in the body) is one event with one sequence number. - Errors are addressed, not broadcast. Rejections return to the sender with its event id; other subscribers never see them.
- Deletion is lifecycle, not content. A delete never appends to the
document’s own event log — it’s recorded in the folder’s membership log, so
a delete → restore round-trip returns the document at the exact sequence it
left, history intact. Deleted documents move from
sys:indexinto the synthesizedsys:trashdocument; subscribe to it and a trash UI updates live, with no dedicated listing request. - Sequences make hydration cheap. Document state can be fetched outside
the socket — say, server-rendered over plain HTTP — and handed to the
client as preloaded state. When the client later subscribes, it sends the
sequence it already holds: the server answers
doc:resume(no data) if nothing changed, or a freshdoc:initif the client is behind. Catch-up is deliberately a full snapshot, not incremental patches — simple over clever, at the cost of re-sending a document that moved one event. The same mechanism makes reconnects cheap — resubscribing with the cached sequence transfers nothing when nothing moved.