docs

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. synced is 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" }
  • status carries exactly the shape of the matching GET, one frame per state transition.
  • lagged appears only if the per-stream forwarder (16 events deep) dropped transitions; total counts the drops since the last successful frame. Re-read the GET to resync — the stream stays open.
  • closed uses 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/health reports bootstrapping: true while 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-status reports, and it becomes meaningful as spaces load. See Server.