Realtime
Sync status
Local-first means a write is done the moment it lands in your store; whether it has reached anyone else is a separate question. Sync status answers it per space and per object, with cheap GETs for a render tick and SSE streams for transitions.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/spaces/:spaceId/sync-status |
rolled-up state of one space |
| GET | /v1/spaces/:spaceId/sync-status/objects/:objectId |
state of one object |
| GET | /v1/sync-status/subscribe |
account-wide SSE — every known space's transitions on one stream |
| GET | /v1/spaces/:spaceId/sync-status/objects/:objectId/subscribe |
per-object SSE |
| POST | /v1/spaces/:spaceId/sync |
force a head-sync round now instead of waiting for the periodic timer |
/v1/spaces/:spaceId/sync-status/peers is registered but answers 501 sdk.not_implemented; the diagnostic GET /v1/spaces/:spaceId/debug is the closest equivalent (see Debugging).
Reading state
curl http://127.0.0.1:7001/v1/spaces/SPACE/sync-status
any sync-status space SPACE
{ "spaceId": "spc_…",
"state": "syncing",
"synced": 1,
"total": 3,
"networkPeers": 0,
"localPeers": 1,
"p2p": "connected",
"lastSyncedAt": "0001-01-01T00:00:00Z" }
| Field | Meaning |
|---|---|
state |
unknown / offline / syncing / synced / error |
synced, total |
objects converged vs. objects tracked in the space |
networkPeers |
responsible sync nodes with a live connection |
localPeers |
LAN peers sharing this space that are connected right now |
p2p |
local-network state: unknown / notpossible / notconnected / connected / restricted (OS denied local-network access) |
lastSyncedAt |
time of the last completed round; zero value until one has run |
A space can be synced with networkPeers: 0 — it converged entirely over the LAN.
Per object:
curl http://127.0.0.1:7001/v1/spaces/SPACE/sync-status/objects/OBJ
any sync-status object SPACE OBJ
{ "objectId": "obj_…", "state": "synced", "lastSyncAt": "2026-05-15T12:00:00Z" }
Unknown object ids return {"state": "unknown"} rather than a 404 — use the object catalog when you need an existence check.
Why it matters. Writes never wait on the network, so a UI needs a separate, honest signal about whether a change has left the device.
syncedis that signal: it comes from the same engine that holds the data, not from a round-trip to a server that may be unreachable.
Streaming transitions
Both /subscribe endpoints are SSE, but they are not the windowed query primitive: transitions are sparse, per-call payloads with no records, no mailbox window, no drifted.
event: ready
data: {}
event: status
data: { …the GET body for the space or object… }
event: lagged
data: { "total": 3 }
event: closed
data: { "reason": "server_shutdown" }
statuscarries exactly the shape of the matching GET, one frame per state transition.laggedappears only if the per-stream forwarder (16 events deep) dropped transitions;totalcounts the drops since the last successful frame. Re-read the GET to resync — the stream stays open.closeduses the shared reason set, so one switch handles every stream family.
The account-wide stream is GET, so a browser can use EventSource directly:
const es = new EventSource("http://127.0.0.1:7001/v1/sync-status/subscribe");
es.addEventListener("status", (e) => {
const s = JSON.parse(e.data);
badge(s.spaceId, s.state); // "syncing" → spinner, "synced" → check
});
curl -N http://127.0.0.1:7001/v1/sync-status/subscribe
any sync-status subscribe # account-wide
any sync-status subscribe SPACE OBJ # one object
The account-wide stream lives outside the /v1/spaces/:spaceId group because one subscription covers every space the account knows; per-object streams sit under the space for symmetry with the GETs.
Forcing a round
Head-sync (diff) rounds against responsible nodes run on a periodic timer (about every 30 s). POST /v1/spaces/:spaceId/sync runs one immediately, blocks until it completes, and returns 204:
curl -X POST http://127.0.0.1:7001/v1/spaces/SPACE/sync
any space sync SPACE
This is what tests use to collapse cross-device convergence waits — sync the writer, then the reader, then read. For ordinary clients the timer plus the status stream is enough; call /sync when a user explicitly asks to "sync now".
Note.
/v1/healthreportsbootstrapping: truewhile the engine's post-start catch-up pass is still running. The server is already serving during that pass; per-space convergence is what/sync-statusreports, and it becomes meaningful as spaces load. See Server.