Realtime
Live space list
The account's list of spaces is itself data — rows in the spaces dataset of the account's private tech space, replicated to every device the account owns. The same windowed query/subscribe primitive that reads any dataset reads that list, so a sidebar can be a live subscription instead of a polling loop.
Endpoints
POST /v1/spaces/query snapshot → { records, total?, hasNext? }
POST /v1/spaces/query/subscribe SSE → ready → snapshot → changes → closed
Both take the standard snapshot body (filter / sort / limit / offset / includeTotal / mailboxCapacity / driftBudgetPercent — see Subscribe) plus an optional dataset. The object is fixed server-side to the tech-space index object; you never pass an objectId.
dataset |
Rows |
|---|---|
spaces (default) |
one row per space the account knows |
profile |
the account's own profile row |
The allowlist is closed: any other name is 400 request.invalid_field. The same index object hosts the identities directory, whose rows carry a synced decryption key — that dataset is deliberately unreachable here and is read through GET /v1/identities instead (see Identities).
curl http://127.0.0.1:7001/v1/spaces/query \
-H 'Content-Type: application/json' \
-d '{"filter":{"status":"active"},"sort":["-createdAt"],"limit":50,"includeTotal":true}'
any space query --filter '{"status":"active"}' --sort -createdAt --limit 50 --total
Raw rows, not SpaceInfo
GET /v1/spaces returns the mapped SpaceInfo shape and defaults to active spaces only. The query and subscribe endpoints return the raw tech-index rows — every status, no projection, system fields included. Use the GET when you want the convenience shape; use query/subscribe when you want filtering, sorting, or liveness.
Fields worth knowing on a raw row:
| Field | Notes |
|---|---|
status |
active, joining, one_to_one_pending, one_to_one_declined, deleted, … — filter on it; deleted rows stay as sticky tombstones |
createdAt |
added-to-account instant, {"$date": "…"}; absent on rows stamped before the field existed. Sort ["-createdAt"], range-filter with {"$gte": {"$date": "…"}} |
ownRole |
your own role, mirrored from ACL state: owner / admin / writer / reader / guest / none |
settings |
the account-private settings object written via PATCH /v1/spaces/:id/settings (e.g. notifyMode) |
push |
{spaceKey, encKey, encKeyId} push key material; absent until the mirror has run for the space |
derived |
set on well-known derived spaces, which cannot be deleted |
What streams as row updates
Because these are plain row fields, changes to them arrive as ordinary updated events on the subscribe stream — no dedicated streams needed:
- A space joined on another device, or one that head-synced in, arrives as
added. - A role change — a grant, promotion or demotion applied by another peer — updates
ownRoleon the row. Role-dependent UI can key off the list subscription instead of fanning outGET …/members/mecalls per space. Caveats:"none"also means "not mirrored yet" (a space this device has not loaded, a pending join, a tombstone), and both participants of a one-to-one space reportwriter;GET /v1/spaces/:id/members/mestays the authoritative per-space read. - A read-key rotation updates
push.encKey/push.encKeyId. Mobile clients keep this stream open while the app is in the foreground so the new key is cached before the first push encrypted under it lands — see Push notifications. - A settings change on another device (muting a space, say) updates
settings. - A deletion on any device arrives as a
removedframe; consumers holding derived per-space state (caches, indexes) purge on it — the server's own search indexer does exactly this.
curl -N http://127.0.0.1:7001/v1/spaces/query/subscribe \
-H 'Content-Type: application/json' \
-d '{"sort":["-createdAt"],"limit":100}'
any space subscribe --sort -createdAt --limit 100 \
| jq 'select(.event=="changes") | .data[] | .updated[]? | {id, ownRole: .doc.ownRole}'
Why it matters. There is no directory service answering "which spaces do I have". The list is CRDT data owned by the account, so every device converges on the same rows — offline joins, renames on a phone, a demotion by a co-owner — without a central lookup, and the list stays readable with no network at all.
Space metadata is elsewhere
A space's name / description / icon are member-replicated, not account-private, and live in a derived in-space spaceIndex object rather than on this row. To live-update a space's title, subscribe to that object in the space itself: take spaceIndexObjectId from GET /v1/spaces/:id and open POST /v1/spaces/:id/objects/query/subscribe filtered on that id. Write it with PATCH /v1/spaces/:id; write account-private settings with PATCH /v1/spaces/:id/settings. See Spaces.
Note. The frame set and
closedreasons are identical to every other windowed subscribe. Recover fromoverflow/drifted/server_shutdown/sdk_closedthe same way: reopen the POST and take the fresh snapshot.