host.kv
host.kv is a small, fast, server-persisted JSON store scoped to the app instance. It is the first reach for state that must survive a reload: a game's high score, a form draft, a toggle, a cursor position. For binary data or large files use host.content instead. For true multi-user CRDT state use host.collab.
API
interface KvNamespace {
get(key: string): Promise<unknown | null>;
set(key: string, value: unknown, opts?: { ttl?: number }): Promise<void>;
incr(key: string, delta?: number, opts?: { ttl?: number }): Promise<number | null>;
delete(key: string): Promise<void>;
list(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;
subscribe(key: string, handler: (value: unknown) => void): () => void;
}| Method | Returns | What it does |
|---|---|---|
get(key) | unknown | null | Read a value, null if absent. |
set(key, value, opts?) | void | Write a JSON value, optional ttl in seconds. |
incr(key, delta?, opts?) | number | null | Atomic server-side value = value + delta. |
delete(key) | void | Remove a key. |
list(prefix?) | { key, value }[] | Enumerate keys, optionally by prefix. |
subscribe(key, handler) | unsubscribe fn | Fire handler on every change to key. |
Read and write
await host.ready();
await host.kv.set('theme', 'dark');
const theme = await host.kv.get('theme'); // 'dark' | nullValues are JSON, so store objects directly. No manual JSON.stringify:
await host.kv.set('draft', { title, body, savedAt: Date.now() });TTL
set and incr take an optional { ttl } in seconds. The key expires server-side after the window. Default is persistent (no expiry).
await host.kv.set('otp', code, { ttl: 300 }); // expires in 5 minutesAtomic counters
incr applies value = value + delta server-side, so concurrent viewers never clobber each other. The stored value must be a JSON number. delta defaults to 1. It returns the new value.
const plays = await host.kv.incr('playCount'); // +1
const score = await host.kv.incr('score', 10); // +10Use incr for any shared tally (play counts, vote totals, live scoreboards). A read-modify-write with get then set is racy across viewers; incr is not.
List by prefix
const entries = await host.kv.list('player:');
// → [{ key: 'player:alice', value: {...} }, { key: 'player:bob', value: {...} }]Namespace keys with a delimiter (player:alice) so list(prefix) can scan a group cheaply. Calling list() with no prefix returns every key in the instance.
Live updates
subscribe fires the handler whenever a key changes, including writes from another viewer of the same shared app. It returns an unsubscribe function. Call it on teardown.
const off = host.kv.subscribe('score', (value) => {
scoreEl.textContent = String(value ?? 0);
});
// later, on unmount:
off();Scope and sharing
- KV is per instance. Two copies of an app on a canvas keep separate stores.
- It participates in the workspace share-cascade: every collaborator who can see the app can read its KV. Do not put secrets here.
- Keep values small. It is a KV store, not a blob store. Reach for host.content once a value is large or binary.
Errors
set and incr can reject with quota_exceeded when the per-instance size or count cap is hit. Other ops surface the standard bridge codes. See error codes.
Related
- host.content: files and binary data
- host.collab: true multi-user CRDT state