# DB-Sync Client-Server Protocol ## Transport - WebSocket `ws(s)` to `/sync/:graph-id`. - Client builds URL from config and appends `?token=...` when available. - Encoding: JSON objects; `tx` payloads are Transit strings. - Note: keep this document in sync with the current implementation. ## Client -> Server - `{"type":"hello","client":""}` - Initial handshake from client. - `{"type":"presence","editing-block-uuid":""}` - Update current editing block for presence (omit or null to clear). - `{"type":"pull","since":}` - Request txs after `since` (defaults to 0). - `{"type":"tx/batch","t-before":,"txs":[{"tx":"","tx-id":"","outliner-op":""}, ...]}` - Upload a batch of txs based on `t-before` (required). - `tx-id` is optional but recommended for per-entry ack/reject mapping. - `{"type":"ping"}` - Optional keepalive; server replies `pong`. ## Server -> Client - `{"type":"hello","t":,"checksum":""}` - Server hello with current t and entity checksum. - `{"type":"online-users","online-users":[{"user-id":"...","email":"...","username":"...","name":"..."}...]}` - Presence update - Optional `editing-block-uuid` indicates the block the user is editing. - `{"type":"pull/ok","t":,"checksum":"","txs":[{"t":,"tx":"","outliner-op":""}...]}` - Pull response with txs and post-apply entity checksum. - `{"type":"tx/batch/ok","t":,"checksum":""}` - Batch accepted; server advanced to t and returns the resulting entity checksum. - `{"type":"changed","t":}` - Broadcast once after a handled `tx/batch` that advanced server state (`t` increased); client should pull. - `{"type":"tx/reject","reason":"stale","t":}` - Client tx is based on stale t. - `{"type":"tx/reject","reason":"db transact failed","t":,"success-tx-ids":["",...],"failed-tx-id":""}` - Server-side transact/validation failed for one tx entry in the batch. - `success-tx-ids` are entries already applied before the failure. - `failed-tx-id` is the entry that failed. - Legacy servers may return `data` with rejected tx payload for debugging. - `{"type":"tx/reject","reason":"empty tx data"|"invalid tx"|"invalid t-before"|"snapshot upload in progress"}` - Invalid batch. - `{"type":"pong"}` - Keepalive response. - `{"type":"error","message":"..."}` - Invalid/unknown message. Current messages: `"unknown type"`, `"invalid request"`, `"server error"`, `"invalid since"`. ## HTTP API - Auth: Bearer token via `Authorization: Bearer ` or `?token=...`. - JSON body/response unless noted. - Auth required for `/graphs`, `/sync/:graph-id/*`, and `/assets/*`. Expect `401` (unauthorized) or `403` (forbidden) on access failure. ### Worker Health - `GET /health` - Worker health check. Response: `{"ok":true}`. ### Graphs (index DO) - `GET /graphs` - List graphs the user owns. Response: `{"graphs":[{graph-id, graph-name, schema-version?, graph-ready-for-use?, created-at, updated-at}...]}`. - `POST /graphs` - Create graph. Body: `{"graph-name":"...","schema-version":""}` (schema-version optional). Response: `{"graph-id":"...","graph-ready-for-use?":false}`. - `graph-ready-for-use?` is persisted in the D1 `graphs` row. Existing graphs default to `true`; bootstrap uploads flip it to `false` until the final snapshot upload request completes. - `GET /graphs/:graph-id/access` - Access check. Response: `{"ok":true}`, `401` (unauthorized), `403` (forbidden), or `404` (not found). - `GET /graphs/:graph-id/members` - Graph members list. Response: `{"members":[{user-id, graph-id, role, invited-by, created-at, email?, username?}...]}`. - `DELETE /graphs/:graph-id` - Delete graph and reset data. Response: `{"graph-id":"...","deleted":true}` or `400` (missing graph id). ### E2EE (index DO) - `GET /e2ee/user-keys` - Fetch current user's RSA key pair. Response: `{"public-key":"","encrypted-private-key":""}` or `{}` when missing. - `POST /e2ee/user-keys` - Upsert current user's RSA key pair. Body: `{"public-key":"","encrypted-private-key":"","reset-private-key":false?}`. - Response mirrors the stored keys: `{"public-key":"","encrypted-private-key":""}`. - `GET /e2ee/user-public-key?email=` - Fetch a user's RSA public key by email. Response: `{"public-key":""}` or `{}` when missing. - `GET /e2ee/graphs/:graph-id/aes-key` - Fetch current user's encrypted graph AES key. Response: `{"encrypted-aes-key":""}` or `{}` when missing. - `POST /e2ee/graphs/:graph-id/aes-key` - Upsert current user's encrypted graph AES key. Body: `{"encrypted-aes-key":""}`. - Response: `{"encrypted-aes-key":""}`. - `POST /e2ee/graphs/:graph-id/grant-access` - Manager-only. Upsert encrypted graph AES keys for members. - Body: `{"target-user-email+encrypted-aes-key-coll":[{"user/email":"","encrypted-aes-key":""}...]}`. - Response: `{"ok":true,"missing-users":["", ...]?}`. ### Sync (per-graph DO, via `/sync/:graph-id/...`) - `GET /sync/:graph-id/health` - Health check. Response: `{"ok":true}`. - `GET /sync/:graph-id/pull?since=` - Same as WS pull. Response: `{"type":"pull/ok","t":,"checksum":"","txs":[{"t":,"tx":"","outliner-op":""}...]}`. - Error response (400): `{"error":"invalid since"}`. - Error response (409): `{"error":"graph not ready"}` when bootstrap upload/import has not finished. - `POST /sync/:graph-id/tx/batch` - Same as WS tx/batch. Body: `{"t-before":,"txs":[{"tx":"","tx-id":"","outliner-op":""}, ...]}`. - Response: `{"type":"tx/batch/ok","t":,"checksum":""}` or `{"type":"tx/reject","reason":...}`. - Error response (400): `{"error":"missing body"|"invalid tx"}`. - Error response (409): `{"error":"graph not ready"}` when bootstrap upload/import has not finished. - `GET /sync/:graph-id/snapshot/download` - Build a snapshot file in R2 and return a download URL. - Response: `{"ok":true,"key":"/.snapshot","url":"/assets/:graph-id/.snapshot","content-encoding":"gzip"}`. - Error response (409): `{"error":"graph not ready"}` when bootstrap upload/import has not finished. - The snapshot file stored in R2 is a framed Transit stream of sqlite `kvs` rows (`[addr, content, addresses]`), optionally gzip-compressed. - `POST /sync/:graph-id/snapshot/upload?reset=true|false` - Upload a snapshot stream for bootstrap import. Current upload format remains framed Transit JSON kvs rows, optionally gzip-compressed. - Request body: binary stream; headers should include `content-type: application/transit+json` and `content-encoding: gzip` when compressed. - Response: `{"ok":true,"count":,"key":"/.snapshot"}`. - Error response (400): `{"error":"missing body"|"missing graph id"}`. - `DELETE /sync/:graph-id/admin/reset` - Drop/recreate per-graph tables. Response: `{"ok":true}`. ### Assets - `GET /assets/:graph-id/:asset-uuid.:ext` - Download asset (binary response, `content-type` set, `x-asset-type` header included). - `PUT /assets/:graph-id/:asset-uuid.:ext` - Upload asset (binary body). Size limit ~100MB. Response: `{"ok":true}`. - `DELETE /assets/:graph-id/:asset-uuid.:ext` - Delete asset. Response: `{"ok":true}`. - Asset error responses: `{"error":"invalid asset path"}` (400), `{"error":"not found"}` (404), `{"error":"asset too large"}` (413), `{"error":"method not allowed"}` (405), `{"error":"missing assets bucket"}` (500).