Skip to content

Key/value store

Reach a key/value store the same way in every runtime with context.kv — pass any unstorage instance, including Cloudflare KV and Vercel Runtime Cache, and read it with getItem/setItem plus TTL.

Updated View as Markdown

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 its Runtime Cache. 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 Runtime Cache, 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 Runtime Cache

Vercel’s Runtime Cache is available in the Vercel runtime with no store to provision and no environment variables to set — pass the driver and go. (It replaces Vercel KV, which Vercel is sunsetting.)

import { Outer } from "@outerjs/server";
import { createStorage } from "unstorage";
import vercelRuntimeCacheDriver from "unstorage/drivers/vercel-runtime-cache";

const outer = new Outer({
  db: { dialect: myDialect, kind: "postgres" },
  kv: createStorage({ driver: vercelRuntimeCacheDriver({ ttl: 60 }) }),
})
  .schema(v1_0)
  .build();

It’s a cache, not durable storage: entries honor ttl (in seconds) but may be evicted early, and the API can’t list keys — getKeys returns [], so getKeys/clear won’t enumerate. Use it for the cache-shaped data above (rate-limit counters, feature flags, idempotency keys), not for anything you can’t recompute.

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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close