ws(s) to /sync/:graph-id.?token=... when available.tx payloads are Transit strings.| State | Event | Next state | Notes / error handling |
| START | send 'hello' | hello-wait | Send immediately after connection opens |
| hello-wait | recv 'hello' | hello-done | Update t; if remote > local then send pull |
| hello-wait | recv other | END | Unexpected message |
| hello-done | send 'pull' | pull-wait | Triggered by hello / changed / stale |
| hello-done | send 'tx/batch' | tx/batch-wait | Flush pending local txs (inflight empty and ws open) |
| hello-done | recv 'changed' | hello-done | If local < remote, send pull then enter pull-wait |
| hello-done | recv 'error' | END | Fail fast |
| hello-done | recv 'pong' | hello-done | Ignore (client does not send ping currently) |
| hello-done | recv other | END | Unknown message type |
| pull-wait | recv 'pull/ok' | hello-done | Apply remote txs, update local t |
| pull-wait | recv 'changed' | pull-wait | If local < remote, re-send pull |
| pull-wait | send 'tx/batch' | tx/batch-wait | Flush pending local txs (inflight empty and ws open) |
| pull-wait | recv 'error' | END | Fail fast |
| pull-wait | recv 'pong' | pull-wait | Ignore (client does not send ping currently) |
| pull-wait | recv other | END | Unknown message type |
| tx/batch-wait | recv 'tx/batch/ok' | hello-done | Update local t, clear inflight, continue flush; stay pull-wait if active |
| tx/batch-wait | recv 'changed' | tx/batch-wait | Mark pull pending; pull after tx/batch completes |
| tx/batch-wait | recv 'tx/reject' (stale) | tx/reject/stale | Handle stale branch |
| tx/batch-wait | recv 'tx/reject' (cycle) | tx/reject/cycle | Handle cycle branch |
| tx/batch-wait | recv 'error' | END | Fail fast |
| tx/batch-wait | recv other | END | Unknown message type |
| tx/reject/stale | send 'pull' | pull-wait | Immediately pull on stale |
| tx/reject/cycle | (reconcile/requeue/flush) | hello-done | Reconcile cycle then resume normal flow (stay pull-wait if active) |
| END | | | Connection closed; reconnection handled elsewhere |
{"type":"hello","client":"<repo-id>"}
{"type":"pull","since":<t>}
since (defaults to 0).{"type":"tx/batch","t_before":<t>,"txs":["<tx-transit>", ...]}
t_before (required).{"type":"ping"}
pong.{"type":"hello","t":<t>}
{"type":"pull/ok","t":<t>,"txs":[{"t":<t>,"tx":"<tx-transit>"}...]}
{"type":"tx/batch/ok","t":<t>}
{"type":"changed","t":<t>}
{"type":"tx/reject","reason":"stale","t":<t>}
{"type":"tx/reject","reason":"cycle","data":"<transit {:attr <kw> :server_values ...}>"}
{"type":"tx/reject","reason":"empty tx data"|"invalid tx"|"invalid t_before"}
{"type":"pong"}
{"type":"error","message":"..."}
"unknown type", "invalid request", "server error", "invalid since".Authorization: Bearer <token> or ?token=..../graphs, /sync/:graph-id/*, and /assets/*. Expect 401 (unauthorized) or 403 (forbidden) on access failure.GET /health
{"ok":true}.GET /graphs
{"graphs":[{graph_id, graph_name, schema_version?, created_at, updated_at}...]}.POST /graphs
{"graph_name":"...","schema_version":"<major>"} (schema_version optional). Response: {"graph_id":"..."}.GET /graphs/:graph-id/access
{"ok":true}, 401 (unauthorized), 403 (forbidden), or 404 (not found).DELETE /graphs/:graph-id
{"graph_id":"...","deleted":true} or 400 (missing graph id)./sync/:graph-id/...)GET /sync/:graph-id/health
{"ok":true}.GET /sync/:graph-id/pull?since=<t>
{"type":"pull/ok","t":<t>,"txs":[{"t":<t>,"tx":"<tx-transit>"}...]}.{"error":"invalid since"}.POST /sync/:graph-id/tx/batch
{"t_before":<t>,"txs":["<tx-transit>", ...]}.{"type":"tx/batch/ok","t":<t>} or {"type":"tx/reject","reason":...}.{"error":"missing body"|"invalid tx"}.GET /sync/:graph-id/snapshot/rows?after=<addr>&limit=<n>
{"rows":[{"addr":<addr>,"content":"<transit>","addresses":<json|null>}...],"last_addr":<addr>,"done":true|false}.POST /sync/:graph-id/snapshot/import
{"reset":true|false,"rows":[[addr,content,addresses]...]}.{"ok":true,"count":<n>}.{"error":"missing body"|"invalid body"}.DELETE /sync/:graph-id/admin/reset
{"ok":true}.GET /assets/:graph-id/:asset-uuid.:ext
content-type set, x-asset-type header included).PUT /assets/:graph-id/:asset-uuid.:ext
{"ok":true}.DELETE /assets/:graph-id/:asset-uuid.:ext
{"ok":true}.{"error":"invalid asset path"} (400), {"error":"not found"} (404), {"error":"asset too large"} (413), {"error":"method not allowed"} (405), {"error":"missing assets bucket"} (500).