KV store

Store cached or shared values when a briefly stale read is acceptable.

On this page

Set up a namespace

Add KV to your existing config:

goribu.config.js
import { defineConfig } from "goribu/server";

export default defineConfig({
  name: "my-app",
  compatibilityDate: "2026-09-01",
  kv: { name: "my-app-kv" },
});
  • Managed binding: 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 use at most 63 characters.
  • Binding changes: Restart development after changing bindings.
  • Local isolation: Local values stay separate from production.
  • Configuration ownership: Do not add a second Wrangler declaration for the namespace.

Read and write

This route returns a cached timestamp, creating it when missing:

src/routes/snapshot.ts
import type { Handler } from "goribu/server";

type Snapshot = { createdAt: string };

// Cache a public snapshot whose age does not affect correctness.
export const GET: Handler = async (req, res) => {
  let snapshot = await req.kv.get<Snapshot>("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);
};
src/routes/snapshot.js
// Cache a public snapshot whose age does not affect correctness.
export const GET = async (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);
};
  • Default reads: get() returns text or null by default.
  • JSON reads: Pass { type: "json" } to parse JSON.
  • JSON writes: Objects need explicit JSON.stringify(); writes have no automatic encoding.

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

MethodOptions 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: get() accepts an object with type, cacheTtl, and metadata. The shorthand get(key, "json") is not supported.
  • Metadata reads: { metadata: true } returns the native value/metadata result. There is no separate req.kv.getWithMetadata() method.
  • Absolute expiration: expiration is an epoch timestamp in seconds.
  • Relative expiration: expirationTtl is a lifetime in seconds.
  • Write completion: Await writes and deletes before reporting success.
  • Platform limits: The native platform validates storage values and limits.
Read a value with metadata
type Snapshot = { createdAt: string };
type Metadata = { source: string };

// Inside a handler with KV configured.
const entry = await req.kv.get<Snapshot, Metadata>("snapshot", {
  type: "json",
  metadata: true,
});
// entry.value is null when the key is missing.
Read a value with metadata
// Inside a handler with KV configured.
const entry = await req.kv.get("snapshot", {
  type: "json",
  metadata: true,
});
// entry.value is null when the key is missing.

Pagination

  • List result: list() returns keys, list_complete, and a cursor when more results are available.
  • Next page: Keep the prefix and limit when requesting the next page. Stop only when list_complete is true.
src/routes/cache-keys.ts
import type { Handler } from "goribu/server";

// Return one page of public cache-key names.
export const GET: Handler = async (req, res) => {
  const cursor = req.query.get("cursor") || undefined;
  const page = await req.kv.list({ prefix: "public:", limit: 100, cursor });
  return res.json(page);
};
src/routes/cache-keys.js
// Return one page of public cache-key names.
export const GET = async (req, res) => {
  const cursor = req.query.get("cursor") || undefined;
  const page = await req.kv.list({ prefix: "public:", limit: 100, cursor });
  return res.json(page);
};
  • Listing access: Only expose key listings appropriate for your app's users.
  • Manual iteration: Goribu does not automatically paginate or retry KV operations.

Choose KV deliberately

  • Stale reads: KV is eventually consistent, including cached misses. After a write, a reader may see an older value or null.
  • Deletion visibility: A deleted value may remain visible temporarily.
  • Code deployments: Deploying new code does not flush KV 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

  • Missing binding: Using KV without its declaration fails with a repair message. Add the config, then restart development or rebuild.
  • Storage failures: 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.