KV Store
KV is a durable key/value store scoped to your app. It is a string store: every value is
persisted as a string. Keys and values belong to your app alone (see the createSdk client in the
SDK Overview).
API
import { createSdk } from "@bext-stack/framework/sdk";
const sdk = createSdk("<app-id>");
await sdk.kv.set("greeting", { hello: "world" }, 3600); // optional TTL in seconds
const value = await sdk.kv.get<{ hello: string }>("greeting");
await sdk.kv.delete("greeting");
const keys = await sdk.kv.list("post:", 100); // prefix + limit
| Method | Description |
|---|---|
get<T>(key) |
Read and decode a value. Returns null when the key does not exist or has expired. |
set(key, value, ttlSecs?) |
Write a value (create or replace), optionally expiring after ttlSecs seconds. |
delete(key) |
Remove a key. Returns true if it existed. |
list(prefix?, limit?) |
List key names, optionally filtered by prefix. limit defaults to 100. |
On failure, every method throws an Error carrying the message returned by the server (or the
HTTP status when there is none). A missing key is not an error: get returns null.
KV reads made during render are served directly by the runtime, so reading KV from a loader or component stays fast.
Semantics
- String store: values are stored as strings.
- TTL: pass
ttlSecs(a whole number of seconds) tokv.setto expire a key. A decimal or negative value is ignored and the key does not expire. An expired key is no longer returned bygetorlist. - Every
setreplaces everything: the value and the expiry. AsetwithoutttlSecson a key that had one makes it permanent; pass the TTL again on each write if you want to keep it. There is no merge: with two concurrent writes to the same key, the last one wins. - Prefix listing:
listaccepts aprefixand alimit, and returns key names only, in no guaranteed order. In the prefix,_and%act as wildcards (any single character, any run of characters) and matching ignores the case of ASCII letters:list("user_")can therefore also returnuserX…orUSER_…. Filter the result withkey.startsWith(prefix)when an exact match matters.
The double-encoding gotcha
The client JSON-stringifies the value you pass to set, and the server stores that string
JSON-encoded once more: the stored value is wrapped twice. On read, the client keeps calling
JSON.parse, up to three times, as long as the result is still a string.
In practice:
- Objects, arrays, numbers and booleans round-trip through
sdk.kvunchanged. - A string that is itself valid JSON does not:
set("k", "42")reads back as the number42, andset("k", '{"a":1}')as an object. Wrap such strings in an object ({ value: "42" }) when the exact string matters. - If you read the raw stored value some other way, expect it JSON-encoded twice.
Pattern: durable job record in KV
The Queue stores no result and has no per-message status. To track a job's outcome, add a durable job record in KV:
// Store a record when you enqueue…
await sdk.kv.set(`job:${id}`, { status: "queued", submittedAt: Date.now() });
// …the worker updates it as it progresses…
await sdk.kv.set(`job:${id}`, { status: "running" });
// …and on completion or failure.
await sdk.kv.set(`job:${id}`, { status: "complete", result });
Each set overwrites the whole record: in this example, submittedAt is gone after the second
call. Read it with get and write the full object back if you want to keep fields.
Use keys like job:<id> with a few small submit / get / complete / fail helpers around them. See
Queue for the enqueue side, or Tasks & Scheduler, whose tasks
already track status for you.
Where to next
| Page | What it covers |
|---|---|
| Cache | TTL caching and how it differs from KV |
| Queue | Fire-and-forget jobs, the job-record pattern |
| Tasks & Scheduler | Long jobs with built-in status and progress |
| SDK Overview | The createSdk client and BextSdk interface |