Realtime
Realtime
Everything live in any arrives over plain HTTP Server-Sent Events from the local server. A subscription is a moving window over a query: one request opens a stream that delivers a snapshot followed by per-change deltas, and the CRDT engine keeps that window correct as writes land from this device or from peers.
Four kinds of live stream
| Stream | Endpoint | What it carries |
|---|---|---|
| Windowed query/subscribe | POST /v1/spaces/:id/objects/query/subscribe, POST /v1/spaces/:id/query/subscribe |
snapshot + added/updated/removed deltas for a filtered, sorted window of records |
| Space list | POST /v1/spaces/query/subscribe |
the same primitive over the account's own space list |
| Sync status | GET /v1/sync-status/subscribe, GET /v1/spaces/:id/sync-status/objects/:objectId/subscribe |
sparse state transitions (syncing → synced …), no records |
| Event bus | GET /v1/events/subscribe |
transient signals published through POST /v1/events — navigation, progress, presence |
A few callback-based streams follow the same envelope for other surfaces: members (GET /v1/spaces/:id/members/subscribe), the identities directory (GET /v1/identities/subscribe) and file status (GET /v1/spaces/:id/files/subscribe). They are documented with their features under Collaboration, Auth and Files.
One envelope, one reason set
Every stream opens with event: ready, emits : keepalive comments every ~25 s while idle, and ends with a terminal event: closed carrying a reason:
| Reason | Meaning |
|---|---|
server_shutdown |
the server is exiting (signal or POST /v1/shutdown) |
sdk_closed |
the underlying subscription was released (space or engine closed) |
overflow |
the subscriber fell behind and its mailbox filled |
drifted |
too much of the held window left without replacement (query/subscribe only) |
All reasons mean the same thing for the client: the stream is over — open a fresh request. There is no replay and no resume cursor; the new snapshot already reflects current state, which is cheaper than shipping the gap.
client any (127.0.0.1:7001)
│ POST …/query/subscribe │
│ ─────────────────────────────▶ │
│ event: ready │
│ event: snapshot {records} │
│ event: changes [{added…}] │ ◀── local write or remote change applied
│ event: changes [{updated…}] │
│ : keepalive │
│ event: closed {reason} │
│ ◀───────────────────────────── │
│ (reopen) │
Why it matters. The subscription engine runs next to the data, on your device. A window is served from the local indexed store, so a subscription costs no network round-trip and keeps working offline — remote changes show up as ordinary
changesframes when sync brings them in.
Why SSE and not WebSocket
The server pushes; the client never sends control frames into an open subscription. SSE is plain HTTP/1.1 — it works through any proxy, is debuggable with curl -N, and needs no upgrade handshake. The one wrinkle is that windowed subscribes are POST (the filter body does not fit a query string), so the browser's EventSource does not apply to them; clients use fetch with a streaming body and parse frames themselves. See Subscribe for a complete example.
Budget your live surface
Subscriptions are the one thing that scales with breadth rather than with data size. Subscribe to what is on screen — one stream per open list, document or chat — and never one stream per object of a collection. Anything you need to know is queryable on demand; a subscription only tells you that it changed. Durable consumers (indexers, notifiers) keep their own cursor and treat events as a wake-up signal.