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.

Tip

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) to kv.set to expire a key. A decimal or negative value is ignored and the key does not expire. An expired key is no longer returned by get or list.
  • Every set replaces everything: the value and the expiry. A set without ttlSecs on 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: list accepts a prefix and a limit, 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 return userX… or USER_…. Filter the result with key.startsWith(prefix) when an exact match matters.

The double-encoding gotcha

Warning

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.kv unchanged.
  • A string that is itself valid JSON does not: set("k", "42") reads back as the number 42, and set("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