KV store
Store cached or shared values when a briefly stale read is acceptable.
Set up a namespace
Add KV to your existing config:
import { defineConfig } from "goribu/server";
export default defineConfig({
name: "my-app",
compatibilityDate: "2026-09-01",
kv: { name: "my-app-kv" },
});Goribu creates the KV binding locally and creates or reuses the named production namespace on deployment. Namespace names use lowercase letters, digits, hyphens, or underscores, start with a letter or digit, and are at most 63 characters.
Restart development after changing bindings. Local values stay separate from production. Do not add a second Wrangler declaration for the namespace.
Read and write
This route returns a cached timestamp, creating it when missing:
// Cache a public snapshot whose age does not affect correctness.
export async function GET(req, res) {
let snapshot = await req.kv.get("snapshot", { type: "json" });
if (snapshot === null) {
snapshot = { createdAt: new Date().toISOString() };
await req.kv.put("snapshot", JSON.stringify(snapshot), {
expirationTtl: 300,
});
}
return res.json(snapshot);
}get() returns text or null by default. Pass { type: "json" } to parse JSON. There is no automatic encoding on writes: use JSON.stringify() for objects.
A successful write is an acknowledgement, not a guarantee that the next reader will see it. This example returns the value it already has instead of depending on an immediate reread.
Operations
| Method | Options and result |
|---|---|
get(key, options?) | Read text, JSON, an ArrayBuffer, or a stream. Missing keys return null. |
put(key, value, options?) | Write a value with optional expiration, expirationTtl, and metadata. |
delete(key) | Delete a key; cached values can remain visible temporarily. |
list(options?) | List one page of keys using prefix, limit, and cursor. |
Read options are type, cacheTtl, and metadata. Use an object, not get(key, "json"). { metadata: true } returns the native value/metadata result; there is no separate req.kv.getWithMetadata() method.
expiration is an epoch timestamp in seconds, while expirationTtl is a lifetime in seconds. Await writes and deletes before reporting success. The native platform validates storage values and limits.
Pagination
A list call returns keys, list_complete, and a cursor when more results are available. Keep the prefix and limit when requesting the next page, and stop only when list_complete is true.
// Return one page of public cache-key names.
export async function GET(req, res) {
const cursor = req.query.get("cursor") || undefined;
const page = await req.kv.list({ prefix: "public:", limit: 100, cursor });
return res.json(page);
}Only expose key listings that are appropriate for your app's users. Goribu does not automatically paginate or retry operations.
Choose KV deliberately
KV is eventually consistent, including cached misses. After a write, a reader may still see an older value or null; after a delete, it may still see the removed value. Deploying new code does not flush those caches.
Use KV for public snapshots, cached content, and data with an acceptable freshness window. Use D1 for authoritative records, exact counters, or a flow that must reliably show a visitor their own changes.
Missing bindings and failures
Using KV without its declaration fails with a repair message. Add the config, then restart development or rebuild. Storage errors propagate through the normal request boundary unless your handler catches them.
For an optional cache, you can handle cache errors and still return the authoritative result. Do not report that a required write succeeded when it failed.