Choosing where data lives
Muhkoo gives you four places to put state, and they’re not interchangeable. The choice comes down to two questions: is the data per-user or shared? and does the server need to read it? Get those right and the primitive picks itself.
The four primitives at a glance
Section titled “The four primitives at a glance”| Primitive | Scope | Server can read it? | Queryable? | Reach for it when… |
|---|---|---|---|---|
client.kv | Per-user | No (encrypted at rest) | No | Private user state: prefs, drafts, small documents |
client.db | App-wide, shared | Yes (by design) | Yes (where/order/limit) | Shared structured data you need to query |
client.storage | Per-Space | No (encrypted) | No | Files & blobs — attachments, images, exports |
client.space | Multi-party | No (E2E) / opt-in | History only | Realtime: chat, channels, collaboration |
Decide in three questions
Section titled “Decide in three questions”- Is it one user’s private data? →
client.kv. It’s sealed under the user’s identity, synced across their devices, and never readable by the server. No schema, no setup. - Do many users share it, and do you need to query it? →
client.db. Define a table in the portal, thenquery/insert/update/deletetyped rows. This is the only primitive the server can filter and sort. - Is it a file, or a live conversation? →
client.storagefor blobs,client.spacefor realtime channels.
Recipes
Section titled “Recipes”Per-user state → client.kv
Section titled “Per-user state → client.kv”UI preferences, a draft in progress, a private journal entry — anything scoped to one user. Encrypted at rest by default, with a live change feed across the user’s own devices.
await client.kv.set("prefs", "ui", { theme: "dark", density: "compact" });const ui = await client.kv.get<UiPrefs>("prefs", "ui");client.kv.on("change", (e) => reconcile(e)); // another device wroteGood for small, document-shaped values you fetch by id. Not for data you need to search or aggregate on the server — it’s ciphertext to the platform.
Shared, queryable data → client.db
Section titled “Shared, queryable data → client.db”A task board, a catalog, a leaderboard — structured rows multiple users read and filter. Define the schema in the portal, then:
const tasks = client.db.table<Task>("tasks");await tasks.insert({ title: "Ship docs", done: false, rank: 1 });const open = await tasks.query({ where: { done: false }, order: { rank: "asc" } });App-scoped and authorized by the app key — every user of the app sees the same
tables. Reach for it whenever you’d otherwise want a WHERE clause.
Files & blobs → client.storage
Section titled “Files & blobs → client.storage”Images, attachments, exports — bytes too big for a KV value. Files are chunked, encrypted, erasure-coded, and content-addressed; the manifest is the capability to read one back. Files live in a Space:
const { manifest } = await client.storage.writeFile({ spaceId, data: file, // a File / Blob / Uint8Array metadata: { name: file.name, type: file.type },});// Share `manifest` (e.g. in a message) to let another member read the file.Realtime collaboration → client.space
Section titled “Realtime collaboration → client.space”A chat channel, a shared cursor, anything users see update live. A Space carries a persisted, end-to-end-encrypted message log and a fan-out websocket. See Messaging.
const channel = await client.space.joinChannel("general");channel.on("message", render);await channel.send({ text: "deploy is live 🎉" });Common combinations
Section titled “Common combinations”Real apps mix these — the skill is using each for what it’s good at:
- Encrypted notes app —
client.kvfor the notes; that’s the whole backend. - Team task board —
client.dbfor the shared tasks (queryable),client.kvfor each user’s private view state,client.spacefor an activity channel. - Chat with attachments —
client.spacefor messages,client.storagefor the files, with the manifest shared as a message.