Skip to content

Spaces

Storage and messaging look like separate features, but they’re two views of one primitive: a space. A space is an addressable unit of state on the edge that holds data and a realtime websocket.

There are two kinds.

One per user, addressed by their commitment. It’s a private key/value store — this is what client.kv reads and writes. Its websocket carries a change feed: when you write from one device, your other devices get a change event. So client.kv.on('change') isn’t a bolted-on extra — it’s the space’s own sync channel.

await client.kv.set("notes", "n1", { body: "" }); // write
client.kv.on("change", (e) => …); // same space, live feed

A multi-party space — the SDK calls it a Space. It holds space-scoped file shards, a persisted message log, and a websocket that fans out frames to everyone present. Two client surfaces ride it:

  • client.message — lightweight realtime:
    • pub/subpublish/subscribe ride plaintext relay frames;
    • direct messagessend('user:x', …) is end-to-end encrypted via the Double Ratchet, delivered in the recipient’s inbox space. Forward-secret and ephemeral: no history, and both parties must be online.
  • client.space — fan-out group channels with history (what group chat is built on). Each space has one symmetric group key; a message is sealed once and is decryptable by every member, so the server can persist it and replay it as history. Channels are named via an app-scoped registry — createChannel / joinChannel / listChannels. See Messaging and the client.space reference.

How members get the group key (server-blind)

Section titled “How members get the group key (server-blind)”

The relay never sees the group key. A new member’s copy is delivered by ECIES-wrapping it to their identity public key, so the server only ever stores opaque blobs. To keep a channel joinable even when no human member is online, each app runs a keeper: a trusted, always-available member that holds the group key and re-issues it to newcomers on join. The keeper can read its channels; the relay + storage stay blind.

Two practical takeaways fall out of the model:

  • Realtime is free. Anything you store or send already has a space behind it, so live updates don’t need extra infrastructure.
  • Encrypted-at-rest ≠ queryable. Personal-space values are sealed client-side, so the server can’t index or query them. storage.list() enumerates ids; filtering happens on the client after decryption. (See Storage.)