WebSocket protocol
Real-time sync uses a JSON WebSocket at /ws. Every frame is a discriminated union with a type field. Schemas live in App/shared/src/ws.ts.
Connection
Section titled “Connection”wss://<host>/ws?token=<bearer_session_token>The server validates the token before the WebSocket upgrade. Invalid or expired tokens get HTTP 401 — no socket.
After connect, the client must send hello as the first application message.
Client → server
Section titled “Client → server”hello (required first)
Section titled “hello (required first)”{ "type": "hello", "since": 42 }since is the highest version this client has successfully applied. Use 0 for a fresh client.
The server replays all rows with version > since as item or delete messages, then sends caught_up.
{ "type": "pong" }Reply to server ping heartbeats. Clients may also send ping in some implementations, but the authoritative heartbeat direction in ws.ts is server → client ping.
Server → client
Section titled “Server → client”{ "type": "item", "item": { "id": "01JC…", "binId": "inbox", "type": "link", "senderDevice": "01JB…", "ts": 1716000000000, "version": 43, "deletedAt": null, "payloadEnc": { "ct": "…", "nonce": "…", "keys": { "01JB…": "…" } }, "blobRef": null, "blobNonce": null, "blobKeys": null }}For blob-backed items, payloadEnc is null and blobRef, blobNonce, blobKeys are set. Fetch download URL via GET /api/blobs/:ref/url, then decrypt locally.
Shape matches ItemRowSchema in shared/src/api.ts.
delete
Section titled “delete”{ "type": "delete", "id": "01JC…", "version": 44}Soft-delete notification. Hide the item locally; there is no trash restore API.
caught_up
Section titled “caught_up”{ "type": "caught_up", "version": 44 }Backlog replay finished. version is the current head. Subsequent messages are live pushes.
{ "type": "ping" }Sent every WS_PING_INTERVAL_MS = 30,000 ms. Client must respond with pong. Server closes the connection if it misses responses for WS_PONG_GRACE_MS = 90,000 ms (two missed pongs).
What is not on the wire
Section titled “What is not on the wire”WsServerMessageSchema does not include an error message type. Malformed client frames are ignored; auth failures happen at HTTP upgrade.
Session lifecycle
Section titled “Session lifecycle”- Connect with bearer token.
- Send
hellowith local version cursor. - Apply replayed
item/deleteframes in order. - On
caught_up, update cursor toversion. - Apply live pushes; respond to
pingwithpong. - On disconnect, exponential backoff reconnect; resume with latest stored version in
hello.
Fanout rules
Section titled “Fanout rules”- Device that wrote an item does not receive it back over WS (already optimistic locally).
- All other connected devices for the tenant receive
itemordeletepushes. - Offline devices catch up on next
helloor RESTGET /api/items?since=N.
Conceptual overview: How sync works.
MCP note
Section titled “MCP note”@syncbins/mcp does not maintain a WebSocket — it polls REST. Same rows, higher latency.