Storage
The SDK has two storage APIs:
client.kv— a per-user, encrypted-at-rest key/value store for small structured values (settings, a to-do, an index). Private to the signed-in user, with a realtime cross-device change feed.client.storage— file storage for blobs of any size, shared through a Space and openable by anyone holding the file’s manifest.
Key/value — client.kv
Section titled “Key/value — client.kv”Collections & ids
Section titled “Collections & ids”Data is organized as collection + id. A collection is just a namespace;
think of it like a table, and the id like a row key.
await client.kv.set("todos", "t1", { title: "Ship docs", done: false });
const todo = await client.kv.get<{ title: string; done: boolean }>("todos", "t1");// → { title: "Ship docs", done: false } (decrypted locally)
const existed = await client.kv.delete("todos", "t1"); // → true
const ids = await client.kv.list("todos"); // → ["t1", "t2", …] (ids only)get returns null for a missing key. list returns the ids in a
collection — not the values (see querying, below).
Encryption
Section titled “Encryption”By default set seals the value with AES-256-GCM before it leaves the device.
You can opt out per call to store plaintext — useful for data that needs to be
readable by something other than this user, or that you’ll encrypt yourself:
await client.kv.set("public-profile", "ada", { handle: "@ada" }, { encrypt: false });get transparently decrypts sealed values and passes plaintext through, so
reads don’t care which way it was written.
Realtime change feed
Section titled “Realtime change feed”Subscribe to changes to the signed-in user’s data — across all their devices:
const off = client.kv.on("change", (e) => { // e.collection, e.id, e.type ("set" | "delete"), e.data (decrypted | null) if (e.collection === "todos") refreshTodos();});
// lateroff();This rides the personal space’s own websocket (see Spaces), so a write on one device updates the others with no extra setup.
Querying
Section titled “Querying”There’s no server-side query. Because values are encrypted client-side, the accelerator can’t index or filter them — that’s the privacy trade-off. List the collection and filter after decryption:
const ids = await client.kv.list("todos");const todos = await Promise.all(ids.map((id) => client.kv.get("todos", id)));const open = todos.filter((t) => t && !t.done);See the To-do app example for a complete feature, and the
client.kv reference for signatures.
Files — client.storage
Section titled “Files — client.storage”client.kv holds small JSON values. For files — anything from a few KB to
gigabytes — use client.storage. It chunks the file, encrypts each chunk with
its own AES-256-GCM key, and Reed–Solomon erasure-codes the chunks into shards
stored in a global, content-addressed store. The output is a manifest:
the per-chunk keys + shard hashes needed to reassemble the file.
The model has three parts worth understanding:
- Files are written to a Space. A file belongs to a SharedSpace (get one
from
client.space); the write is metered against that space.writeFilerequires aspaceId— there’s no “personal” file write. - The manifest is the capability. Shard bytes are open ciphertext; the
manifest holds the keys. Anyone you give the manifest to can open the file
from anywhere via
readByManifest— that’s how you share a file. The manifest is stored in the Space’s gated store where Muhkoo can read it, so stored files are encrypted at rest but not hidden from the platform;client.kvand direct messages are the private-from-the-platform stores. - Your shared files are discoverable. On each write the SDK also records the
manifest in your own space, so
listFiles()(no argument) shows the files you’ve shared without enumerating every Space.
// Write a file into a space (obtained from client.space).const { stat, manifest } = await client.storage.writeFile({ spaceId, data: file, // Uint8Array | Blob | File metadata: { name: file.name, type: file.type },});
// Read it back — by space + id, or straight from the manifest (the capability).const { data } = await client.storage.readFile(spaceId, stat.id);const shared = await client.storage.readByManifest(manifest); // no membership needed
// Discover + manage.const mine = await client.storage.listFiles(); // files I've sharedconst inSpace = await client.storage.listFiles({ spaceId }); // files in a spaceawait client.storage.deleteFile(spaceId, stat.id);See the client.storage reference for full signatures
and the encrypted chat example for files in practice.
Reading many files at once
Section titled “Reading many files at once”A shard is not a file: every file becomes chunks, and every chunk becomes shards, so a directory of small files is thousands of tiny reads. Two things keep that from costing a request each.
Shard reads issued at the same moment coalesce into one batched request (up to 256 shards), and a read fetches only the data shards — parity is fetched lazily, when a data shard is actually missing, rather than downloaded and discarded on every healthy read. Neither changes the API: you ask for a file and get a file.
Opening the same 519-file project in the Muhkoo IDE went from 2,086 requests to 22, 18.4s to 8.8s in Chromium and 37.5s to 14.9s in Brave. Brave gained the most because it filters every request — round trips were the cost, not bytes.
The one thing this asks of your code is that the reads actually overlap:
// Coalesces: every file's shard reads are in flight together.const all = await Promise.all( manifests.map((m) => client.storage.readByManifest(m)),);
// Doesn't: each file finishes before the next starts, so the batcher never// has more than one shard hash to work with.const oneByOne = [];for (const m of manifests) oneByOne.push(await client.storage.readByManifest(m));Reading in sequence is the one case batching cannot help — and batching with too
few reads in flight is worse than none at all, because it trades parallelism for
a round trip. That is why the SDK reads concurrently alongside it (8 shard reads
in flight, and concurrent directory fetches while client.vfs
walks a tree); the two only work together. See
How reads are fetched for the
mechanics and the batchShards escape hatch.
Peer-to-peer transfer
Section titled “Peer-to-peer transfer”Members of a Space can fetch file shards from each other before falling back
to the origin. This applies to files read through a Space (space.getFile /
space.getFileStream), in browsers only; client.storage and client.vfs
reads go to origin. Opt in when constructing the client:
new Client({ apiKey, p2p: { enabled: true, workerFactory: () => // keeps it off the main thread new Worker(new URL("@muhkoo/connect/p2p-worker", import.meta.url), { type: "module" }), },});| Option | Default | Description |
|---|---|---|
enabled | false | Turn peer exchange on. |
workerFactory | — | Build the Web Worker that runs the block engine. Without it the SDK tries its bundled worker, then falls back to the main thread. |
iceServers | platform defaults | RTCIceServer[] for NAT traversal. |
maxPeers | — | Cap simultaneous peer connections. |
debug | false | Log mesh and block activity to the console. |
It is an optimisation, never a dependency: a shard no peer has, or a peer that does not answer within the timeout, falls back to origin. Nothing fails because a peer was unavailable, and turning it off changes performance rather than behaviour.
Shards are content-addressed and individually encrypted, so a peer serving you a block cannot read it and cannot forge it — the hash is checked on arrival. What peers do learn is which shards you asked for, which is why this is opt-in and scoped to members of a Space you are already in.