This website works better with JavaScript
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":"<repo-id>"}
Initial handshake from client.
{"type":"presence","editing-block-uuid":"<uuid|null>"}
Update current editing block for presence (omit or null to clear).
{"type":"pull","since":<t>}
Request txs after since (defaults to 0).
{"type":"tx/batch","t-before":<t>,"txs":[{"tx":"<tx-transit>","tx-id":"<uuid?>","outliner-op":"<keyword?>"}, ...]}
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":<t>,"checksum":"<hex>"}
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":<t>,"checksum":"<hex>","txs":[{"t":<t>,"tx":"<tx-transit>","outliner-op":"<keyword?>"}...]}
Pull response with txs and post-apply entity checksum.
{"type":"tx/batch/ok","t":<t>,"checksum":"<hex>"}
Batch accepted; server advanced to t and returns the resulting entity checksum.
{"type":"changed","t":<t>}
Broadcast once after a handled tx/batch that advanced server state (t increased); client should pull.
{"type":"tx/reject","reason":"stale","t":<t>}
Client tx is based on stale t.
{"type":"tx/reject","reason":"db transact failed","t":<t>,"success-tx-ids":["<uuid>",...],"failed-tx-id":"<uuid>"}
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"}
{"type":"pong"}
{"type":"error","message":"..."}
Invalid/unknown message. Current messages: "unknown type", "invalid request", "server error", "invalid since".
HTTP API
Auth: Bearer token via Authorization: Bearer <token> 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":"<major>"} (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":"<transit>","encrypted-private-key":"<transit>"} or {} when missing.
POST /e2ee/user-keys
Upsert current user's RSA key pair. Body: {"public-key":"<transit>","encrypted-private-key":"<transit>","reset-private-key":false?}.
Response mirrors the stored keys: {"public-key":"<transit>","encrypted-private-key":"<transit>"}.
GET /e2ee/user-public-key?email=<email>
Fetch a user's RSA public key by email. Response: {"public-key":"<transit>"} or {} when missing.
GET /e2ee/graphs/:graph-id/aes-key
Fetch current user's encrypted graph AES key. Response: {"encrypted-aes-key":"<transit>"} or {} when missing.
POST /e2ee/graphs/:graph-id/aes-key
Upsert current user's encrypted graph AES key. Body: {"encrypted-aes-key":"<transit>"}.
Response: {"encrypted-aes-key":"<transit>"}.
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":"<email>","encrypted-aes-key":"<transit>"}...]}.
Response: {"ok":true,"missing-users":["<email>", ...]?}.
Sync (per-graph DO, via /sync/:graph-id/...)
GET /sync/:graph-id/health
Health check. Response: {"ok":true}.
GET /sync/:graph-id/pull?since=<t>
Same as WS pull. Response: {"type":"pull/ok","t":<t>,"checksum":"<hex>","txs":[{"t":<t>,"tx":"<tx-transit>","outliner-op":"<keyword?>"}...]}.
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":<t>,"txs":[{"tx":"<tx-transit>","tx-id":"<uuid?>","outliner-op":"<keyword?>"}, ...]}.
Response: {"type":"tx/batch/ok","t":<t>,"checksum":"<hex>"} 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":"<graph-id>/<uuid>.snapshot","url":"<origin>/assets/:graph-id/<uuid>.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":<n>,"key":"<graph-id>/<uuid>.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).