KV Store
KV is a durable, app-scoped key/value store. It is a string store: every value is persisted as a string. Keys and values are scoped to your app id (see the loopback model in the SDK Overview).
JS API
import { createSdk } from "@bext-stack/framework";
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. |
set(key, value, ttlSecs?) |
Write a value, optionally expiring after ttlSecs. |
delete(key) |
Remove a key. |
list(prefix?, limit?) |
List keys, optionally by prefix and capped by limit. |
In-render KV reads are short-circuited in-process (~20µs) — reading KV from a loader or component does not pay a full loopback round trip.
Endpoints
All endpoints are POST and scoped by X-Bext-App-Id (see SDK Overview):
| Endpoint | Body | Returns |
|---|---|---|
/kv/get |
{ key } |
{ value } |
/kv/set |
{ key, value, ttl? } |
— |
/kv/delete |
{ key } |
{ deleted } |
/kv/list |
{ prefix?, limit? } |
{ keys } |
/kv/entries |
{ prefix?, limit? } |
keys and values |
curl -s http://127.0.0.1/__bext/sdk/kv/set \
-H "X-Bext-App-Id: <app-id>" \
-H "Content-Type: application/json" \
-d '{"key":"greeting","value":"hi","ttl":3600}'
Semantics
- String store — values are stored as strings.
- TTL — pass
ttl(seconds) toset/ttlSecstokv.setto expire a key. - Prefix listing —
list/entriesaccept aprefixand alimit.
The double-encoding gotcha
The JS client JSON-stringifies the value you pass to set, and the server stores that
string. Because a value can be stringified more than once along the way, a stored value may
end up JSON-wrapped up to ~3×. On read, the client "peels" it by calling
JSON.parse up to 3 times to recover the original value.
Practical consequences:
- Round-tripping through
sdk.kvis safe — the client re-parses on read. - If you read the raw string a different way (e.g. via
/kv/entriesdirectly, or a different runtime), you may see a value that is JSON-wrapped one to three times and mustJSON.parseit yourself the same number of times.
Pattern: durable job record in KV
The Queue has no result store — it is fire-and-forget with no per-message status. When you need to track a job's outcome, layer 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 });
Use keys like job:<id> with small submit / get / complete / fail helpers around them. See
Queue for the enqueue side, or Tasks & Scheduler whose
task-executor already tracks status for you.
Where to next
| Page | What it covers |
|---|---|
| Cache | TTL scratch cache — how it differs from KV |
| Queue | Fire-and-forget jobs; the KV job-record pattern |
| Tasks & Scheduler | Long jobs with built-in status/progress |
| SDK Overview | The loopback SDK and BextSdk interface |