Skip to content

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.storagefile storage for blobs of any size, shared through a Space and openable by anyone holding the file’s manifest.

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

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.

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();
});
// 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.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.

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. writeFile requires a spaceId — 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.
  • 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 shared
const inSpace = await client.storage.listFiles({ spaceId }); // files in a space
await client.storage.deleteFile(spaceId, stat.id);

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