Key/value store
context.kv is a key/value store for the data that doesn't belong in your schema: cache entries, rate-limit counters, ephemeral tokens, feature flags, idempotency keys. You pass any unstorage instance as new Outer({ kv }), and every handler reads it the same way — a VPS backed by memory or Redis, Cloudflare Workers backed by KV, or Vercel backed by Vercel KV. The backend difference lives in the driver, not your procedures.
It's for values you want fast and optionally expiring. For anything you query, relate, or need transactions on, use context.db instead — the KV store has no queries, joins, or transactions.
Pass a store on the constructor
Point kv at an unstorage instance. In new code, wire it once at construction rather than injecting a store through middleware:
import { Outer } from "@outerjs/server";
import { pglite } from "@outerjs/server/pglite";
import { createStorage } from "unstorage";
const outer = new Outer({ db: pglite(), kv: createStorage() })
.schema(v1_0)
.procedure("cache.get", (base) =>
base.handler(async ({ context }) => {
const hit = await context.kv.getItem<{ value: string }>("greeting");
if (hit) return hit;
const fresh = { value: "hello" };
await context.kv.setItem("greeting", fresh, { ttl: 60 }); // expires in 60s
return fresh;
}),
)
.build();
createStorage() with no driver is an in-memory Map — good for local development and tests. Swap the driver for your platform's store and nothing in your handlers changes.
The store
context.kv is typed as OuterKV, the slice of unstorage's Storage most apps use:
type OuterKV = {
hasItem(key: string): Promise<boolean>;
getItem<T = unknown>(key: string): Promise<T | null>;
getItemRaw(key: string): Promise<unknown>;
setItem(key: string, value: unknown, opts?: { ttl?: number }): Promise<void>;
setItemRaw(key: string, value: unknown, opts?: { ttl?: number }): Promise<void>;
removeItem(key: string): Promise<void>;
getKeys(base?: string): Promise<string[]>;
clear(base?: string): Promise<void>;
};
getItem / setItem serialize JSON; use the Raw variants for binary values. ttl is in seconds and is honored by the drivers that support expiry (Cloudflare KV, Vercel KV, Redis) — the in-memory driver ignores it. getKeys(base) and clear(base) scope to a key prefix.
context.kv is undefined when you don't pass kv. Guard for it, or make it required in your own context type.
Cloudflare KV
Bind a KV namespace in wrangler.toml, then wrap the binding with unstorage's Cloudflare KV driver:
import { Outer } from "@outerjs/server";
import { createStorage } from "unstorage";
import cloudflareKVBinding from "unstorage/drivers/cloudflare-kv-binding";
export default {
fetch(req: Request, env: Env) {
const outer = new Outer({
db: { dialect: myDialect, kind: "sqlite" },
kv: createStorage({ driver: cloudflareKVBinding({ binding: env.MY_KV }) }),
})
.schema(v1_0)
.build();
return outer.handle(req);
},
};
Construct the store inside fetch (or your Durable Object constructor), where the env binding is available. Cloudflare KV honors ttl down to a 60-second minimum.
Vercel KV
Set the KV_REST_API_URL and KV_REST_API_TOKEN environment variables from your Vercel KV store, then use the Vercel KV driver:
import { Outer } from "@outerjs/server";
import { createStorage } from "unstorage";
import vercelKVDriver from "unstorage/drivers/vercel-kv";
const outer = new Outer({
db: { dialect: myDialect, kind: "postgres" },
kv: createStorage({ driver: vercelKVDriver() }),
})
.schema(v1_0)
.build();
Vercel KV is Redis-backed, so ttl works with no minimum.
Nitro and other hosts
When Outer runs inside a host that already exposes unstorage — Nitro's useStorage() — pass it straight through. The driver is chosen in the host's config, so moving to Cloudflare KV or Redis in production needs no change here:
import { useStorage } from "nitro/storage";
new Outer({ db: pglite(), kv: useStorage() });
Redis, filesystem, and more
Any unstorage driver works — redis, fs, upstash, netlify-blobs, and others. Pick the one that matches your deployment and pass it to createStorage({ driver }); context.kv stays the same in every procedure.