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.
Personal space
Section titled “Personal space”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: "…" }); // writeclient.kv.on("change", (e) => …); // same space, live feedShared space
Section titled “Shared space”A multi-party space — the SDK calls it a Space. It holds a persisted
message log, optionally a signed member list (for private spaces), and a websocket that fans out frames to everyone present. Two
client surfaces ride it:
client.message— lightweight realtime:- pub/sub —
publish/subscriberide plaintext relay frames; - direct messages —
send('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.
- pub/sub —
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 theclient.spacereference.
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. Because the keeper is run by the platform, Muhkoo can read channel messages; the relay and storage on their own see only ciphertext. Direct messages, client.kv and zero-knowledge sign-in are the parts the platform cannot read.
File bytes are not stored in a space. They go to a global, content-addressed
shard store as encrypted shards, and the manifest holding the keys lives wherever
you put it — the space’s gated store for client.storage, a message for a chat
file, or a sealed record in your personal space for client.vfs.
Why this matters
Section titled “Why this matters”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.
client.kv.list()enumerates ids; filtering happens on the client after decryption. (See Storage.)