Skip to content

Storage

This content is for the 0.2.0-alpha.3 version. Switch to the latest version for up-to-date documentation.

client.storage is a per-user key/value store. Values are encrypted at rest by default with a key derived from the signed-in user’s identity, so the accelerator only ever holds ciphertext.

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.storage.set("todos", "t1", { title: "Ship docs", done: false });
const todo = await client.storage.get<{ title: string; done: boolean }>("todos", "t1");
// → { title: "Ship docs", done: false } (decrypted locally)
const existed = await client.storage.delete("todos", "t1"); // → true
const ids = await client.storage.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).

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.storage.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.

Subscribe to changes to the signed-in user’s data — across all their devices:

const off = client.storage.on("change", (e) => {
// e.collection, e.id, e.type ("set" | "delete"), e.data (decrypted | null)
if (e.collection === "todos") refreshTodos();
});
// later
off();

This rides the personal space’s own websocket (see Spaces), so a write on one device updates the others with no extra setup.

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.storage.list("todos");
const todos = await Promise.all(ids.map((id) => client.storage.get("todos", id)));
const open = todos.filter((t) => t && !t.done);

See the To-do app example for a complete feature, and the client.storage reference for signatures.

client.storage holds small JSON values. For files — anything from a few KB to gigabytes — use FileStorage. It splits a file into fixed-size chunks (4 MiB by default), encrypts each chunk with its own AES-256-GCM key, then Reed–Solomon erasure-codes every chunk into data + parity shards. Shards are stored content-addressed (named by the SHA-256 of their ciphertext), so a lost shard can be reconstructed from the others and identical shards dedupe for free.

The output of a write is a manifest: the per-chunk keys, IVs, and shard hashes needed to reassemble the file. The shards are useless ciphertext without it — so the manifest is the thing you protect.

  • ShardClient (always required) — talks to the open, content-addressed shard store (PUT/GET /api/shards/:hash). Anyone can put or get a shard, but without the manifest’s keys the bytes are noise.
  • SharedSpaceClient (optional) — a ZK-gated, multi-user store that holds manifests behind a participant ACL. Only needed for the space-managed mode.
import { FileStorage, ShardClient } from "@muhkoo/connect";
const storage = new FileStorage({
shards: new ShardClient({ baseUrl: "https://api.muhkoo.dev" }),
// chunkSize, dataShards, parityShards, concurrency all have sensible defaults
});

Write to shards and get the manifest back; deliver it to readers yourself (for example, as an end-to-end-encrypted chat message). Nothing is stored server-side except the opaque shards.

const { manifest, stat } = await storage.writeFileToShards({
data: file, // Uint8Array | Blob | File
metadata: { name: file.name, type: file.type },
});
// …deliver `manifest` to the reader over an encrypted channel…
const { data } = await storage.readFileFromShards(manifest); // Uint8Array

Mode B — a gated space carries the manifest

Section titled “Mode B — a gated space carries the manifest”

Pass a SharedSpaceClient and the manifest is stored in a multi-user space behind its ACL; participants read, list, and delete by file id.

import { FileStorage, ShardClient, SharedSpaceClient } from "@muhkoo/connect";
const storage = new FileStorage({
shards: new ShardClient({ baseUrl }),
space: new SharedSpaceClient({ baseUrl, /* commitment, secret, salt, circuits, … */ }),
});
const stat = await storage.writeFile({ spaceId, data: file, metadata: { name, type } });
const { data } = await storage.readFile(spaceId, stat.id);
const files = await storage.listFiles(spaceId); // FileStat[]
await storage.deleteFile(spaceId, stat.id);

See the client.storage reference for full signatures and the encrypted chat example for files in practice.