Skip to content

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.

EventMeaning
doc:subscribeStart receiving changes for a document. Server replies with doc:init (or doc:notfound).
doc:unsubscribeStop receiving changes.
doc:createCreate a document — id, type, initial data, optionally with embedded Yjs documents.
doc:updateChange 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:deleteSoft-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:restoreUndo a soft delete — the document re-enters the index at the exact sequence it left, and subscribers receive a fresh doc:init.
doc:get-eventsFetch a document’s event log, optionally bounded by sequence range.
EventMeaning
doc:initFull snapshot at subscribe time — sequence, type, data, and the state of embedded Yjs documents.
doc:patchAn 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:errorA rejected event — categorized (validation failure, failed guard, limit), tied back to the client event id. Guard failures are expected signals, not faults.
doc:notfoundThe subscribed document doesn’t exist.
doc:deletedThe 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:resumeThe connection was re-established; client and server resynchronize subscriptions.
  • Deltas dominate. After doc:init, a subscriber only ever receives patches — full snapshots are never re-broadcast.
  • One update, two payloads. A single doc:update carries 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:index into the synthesized sys:trash document; 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 fresh doc:init if 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.