KV store
Every app has one key-value store. Keys are strings, values are any JSON-serialisable data, and everyone the app reaches shares the same store — whether they opened it from a group, a direct invite, or a link. It is the app’s database: lists, votes, scores, settings, references to Drive files.
interface MaypopKvEntry { key: string; value: unknown; author: string | null; updatedAt: string;}
interface MaypopKv { get(key: string): Promise<unknown>; set(key: string, value: unknown): Promise<void>; delete(key: string): Promise<void>; list(opts?: { prefix?: string }): Promise<MaypopKvEntry[]>; subscribe(prefix: string, cb: (entries: MaypopKvEntry[]) => void): () => void;}A shared to-do list
Section titled “A shared to-do list”await maypop.ready();
// Render whatever is in the store now, and again on every change —// including changes other people make.const stop = maypop.kv.subscribe("todo/", (entries) => render(entries));
async function add(text: string) { if (maypop.mode !== "read-write") return; await maypop.kv.set(`todo/${crypto.randomUUID()}`, { text, done: false, authorId: maypop.user!.id, });}How it behaves
Section titled “How it behaves”- Local first. Each device keeps a replica.
setanddeleteresolve as soon as the change is in that replica; it syncs in the background and survives going offline. - Live.
subscribecalls back immediately with the current entries and again whenever anything under the prefix changes, on any device. - Subscribe for anything on screen.
getandlistread the local replica once and can be empty or stale while it is still loading. - Last write wins. When two people set the same key, the one the server commits last stays. There are no transactions or atomic increments; give each person or item its own key instead of editing one shared counter.
- A rejected write rolls back. If Maypop refuses a write — too large, or
not allowed by the app’s policy — the value disappears from the replica
after
sethas already resolved. Validate size and permission up front. listreturns entries sorted by key.authoris the writer’s app-scopeduser.id.- Keys starting with
~are reserved for the SDK; don’t write them.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Key | 1,024 bytes (UTF-8) |
| Value | 256 KiB of serialised JSON |
| Synced to one device | 5,000 entries or 5 MiB, when a policy is active |
Store large things — photos, audio, long documents — in
Drive and keep the file’s cid in KV.
Per-person data
Section titled “Per-person data”There is one store, so a key containing someone’s id is not private by itself. To keep data per person:
- Put the viewer’s id in the key:
private/${maypop.user.id}/settings. - Add a saved-data policy that only lets that person sync it.
Saved-data policy
Section titled “Saved-data policy”A policy decides who can read and write which keys. It lives in your app’s
source as .maypop/kv-policy.json and takes effect when you publish a
version that contains it. A version without the file keeps the policy that
is already active; an invalid file fails the publish. Without any policy,
everyone who can open the app can read every key, and writers can change any
of them.
Presets
Section titled “Presets”{ "schemaVersion": 1, "mode": "owner_private", "rules": [] }mode |
Who can do what |
|---|---|
shared_group_data |
Everyone with access reads everything; writers and above change anything |
owner_private |
Each person reads and writes only private/{memberId}/**; visitors see nothing |
group_read_admin_write |
Everyone reads; only editors and admins write |
advanced |
Your own rules |
A rule allows actions to roles on keys matching key. Anything no rule
allows is denied.
| Field | Values |
|---|---|
actions |
read, sync, create, update, delete |
roles |
guest (readers and visitors), user (writers), admin (editors), owner (app admins) |
key |
/-separated pattern: literals, * for one segment, {memberId} for the viewer’s id, ** as the last segment for everything below |
owner |
null, { "source": "entryAuthor" }, or { "source": "jsonPointer", "pointer": "/authorId" } |
Roles match exactly — list every role a rule applies to. sync is what lets
data reach a device at all, so it decides what a person can see; give it
alongside read. An owner condition limits a rule to records the viewer
wrote, or whose field at pointer holds their id. On update, both the old
and new record must match, so nobody can hand a record to someone else.
Private to-dos per person, plus a shared board where people edit their own posts and the app admin can remove any:
{ "schemaVersion": 1, "mode": "advanced", "rules": [ { "actions": ["read", "sync", "create", "update", "delete"], "roles": ["user", "admin", "owner"], "key": "todo/private/{memberId}/**", "owner": null }, { "actions": ["read", "sync"], "roles": ["guest", "user", "admin", "owner"], "key": "todo/shared/**", "owner": null }, { "actions": ["create", "update", "delete"], "roles": ["user", "admin", "owner"], "key": "todo/shared/**", "owner": { "source": "jsonPointer", "pointer": "/authorId" } }, { "actions": ["delete"], "roles": ["owner"], "key": "todo/shared/**", "owner": null } ]}A policy can have up to 128 rules. Anyone who can publish the app can change its policy, and the change applies as soon as that version is live.
Test a policy locally with "strictStorage": true in
.maypop/local/dev.json.