DeepSpace syncs data between clients over a persistent WebSocket connected to a Durable Object. This page covers the wire protocol, how writes propagate, and the consistency guarantees the SDK provides.Documentation Index
Fetch the complete documentation index at: https://docs.deep.space/llms.txt
Use this file to discover all available pages before exploring further.
The model
Every client holds a local replica of the records it has subscribed to. When any client mutates a record, the DO validates the write against permissions, persists it to SQLite, and broadcasts the update to every other connected client within milliseconds. EachRecordRoom is the single source of truth for its data. The DO owns its SQLite database and serializes all writes, so there are no merge conflicts to resolve.
The mutation pipeline
useMutations returns three mutation functions - create, put, remove - plus a *Confirmed variant of each. The plain functions are fire-and-forget over the WebSocket; the *Confirmed variants await a server ACK and reject on failure.
create:
- Generates a
recordIdclient-side and sends acore.putmessage over the WebSocket - The DO verifies RBAC, writes to SQLite, and broadcasts
core.record_changeto every connected client whose RBAC allows it (including the sender) - The client store applies the change against any active
useQuerysubscriptions and re-renders
*Confirmed variants when you need to know the server accepted the write - typically to surface a permission error inline, or to wait on a server-validated outcome before navigating.
Subscription scope
A client subscribes to one or more scopes (Durable Object instances). The default scope in the scaffold isapp:<APP_NAME> - every record in your app’s main RecordRoom syncs to every connected client whose RBAC allows it.
You can mount additional scopes by nesting <RecordScope>:
What the DO sends
When a client callsuseQuery, the SDK sends a core.subscribe message. The DO replies with a core.query_result snapshot containing every record that matches the query and passes the caller’s read check. From that point forward, the DO pushes incremental updates as core.record_change messages, each carrying a changeType: 'create' | 'update' | 'delete' discriminator alongside the record envelope.
The client store applies each change against every active subscription. An update can move a record into or out of a query’s where clause, so the same changeType: 'update' can mean different things to different subscriptions:
| Wire message | Effect on a subscription |
|---|---|
changeType: 'create', record matches where | Record is added to the result set |
changeType: 'update', record matches and was already in the set | Record is updated in place |
changeType: 'update', record now matches but wasn’t in the set | Record is added (treated as a create) |
changeType: 'update', record no longer matches and was in the set | Record is removed (treated as a delete) |
changeType: 'delete' | Record is removed if present |
Consistency guarantees
- Writes are serialized inside the DO. Two concurrent
puts land in a deterministic order - the second wins on field-level merge. - The DO sees all writes before any client. There is no eventual consistency window from the DO’s perspective.
- WebSocket disconnects trigger an automatic reconnect. Active subscriptions re-subscribe and receive a fresh snapshot; the client store reconciles silently.
- In-flight
*Confirmedcalls reject if the socket drops with'WebSocket disconnected'; calls made while already offline reject with'WebSocket not connected'. Fire-and-forget mutations sent while disconnected are silently dropped - usecreateConfirmed/putConfirmed/removeConfirmedwhen you need delivery guarantees.
Permissions on the wire
Permissions are enforced before the DO broadcasts. A user without read access to a record never sees it on the wire, so client-side filters are not a security boundary - they’re a usability concern. If a schema declares avisibilityField, the DO re-evaluates read access on every update. When a record’s value at that field transitions to the configured “visible” value (default: 'public'), the DO broadcasts a core.record_change to clients that gain read access at that moment. The inverse also happens: making a record private produces a changeType: 'update' that the client store interprets as a delete for users who lose access. See permissions for the full visibility model.
Other room types
The same WebSocket pattern applies to the other DO types exported fromdeepspace/worker, but the wire vocabulary differs:
YjsRoomspeaks the Yjs sync protocol. The DO holds the canonical Y.Doc and broadcasts updates.CanvasRoomstores shapes in a Y.DocY.Mapand sends typed shape and viewport messages on top, optimized for high-frequency cursor and shape updates.PresenceRoomis fire-and-forget - peer state is held in memory only, never persisted. Updates broadcast at full speed.GameRoomruns an authoritative tick loop on the DO, broadcasting state snapshots to all players.
useQuery / useMutations for durable, RBAC-filtered records. Use the room-specific hooks - useYjsText, useCanvas, usePresenceRoom, useGameRoom - for CRDT text, canvas shapes, presence, or a server-authoritative tick loop.
Disconnection and reconnection
The SDK handles WebSocket lifecycle transparently:- On disconnect, the local store stays intact - UI keeps working with the last known snapshot.
- The SDK reconnects with exponential backoff (capped at 30s) and on tab refocus.
- On reconnect, every active subscription re-subscribes and receives a fresh
core.query_resultsnapshot. - In-flight
*Confirmedcalls reject with'WebSocket disconnected'; calls made after the socket has closed reject with'WebSocket not connected'. Fire-and-forget calls made while offline are dropped - the SDK does not queue them.
useQuery’s status returns 'loading' | 'ready' | 'error'; usePresenceRoom exposes a connected boolean).
Next steps
- Data storage - define a schema and wire up CRUD.
- Presence and cursors - real-time presence with
usePresenceRoom. - Collaborative editing - Yjs-backed text and shapes.
- Records reference - the full hooks API.