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.
Tip

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) to set / ttlSecs to kv.set to expire a key.
  • Prefix listinglist / entries accept a prefix and a limit.

The double-encoding gotcha

Warning

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.kv is safe — the client re-parses on read.
  • If you read the raw string a different way (e.g. via /kv/entries directly, or a different runtime), you may see a value that is JSON-wrapped one to three times and must JSON.parse it 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