Quickback Docs

KV Storage

Cloudflare Workers KV is a global key-value store optimized for read-heavy workloads. Quickback wires KV in as Better Auth's secondaryStorage (a session cache in front of the auth database), as the JWT-revocation store, and as a general-purpose store for caching and fast lookups.

What is KV?

Workers KV is a distributed key-value store:

  • Global distribution - Data replicated to 300+ edge locations
  • Low-latency reads - Cached at the edge, sub-millisecond access
  • Eventually consistent - Writes propagate globally within 60 seconds
  • Simple API - Get, put, delete, list

Use Cases in Quickback

Use CaseDescription
Session cacheWired as Better Auth's secondaryStorage — a read-through cache in front of the auth database
JWT revocationThe store behind auth.jwt.revocationCheck: 'kv'
CacheCache expensive database queries or API responses
Rate LimitingTrack request counts per user/IP
Feature FlagsStore configuration that changes infrequently

Namespace Setup

Create a KV namespace via Wrangler:

# Create namespace
wrangler kv namespace create "KV"

# Output:
# Add the following to your wrangler.toml:
# [[kv_namespaces]]
# binding = "KV"
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Setting the namespace ID in config

To have the compiler generate wrangler.toml with your KV namespace ID, set it in your provider config:

providers: {
  storage: defineStorage("cloudflare-kv", {
    binding: "KV",           // default
    namespaceId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  }),
}

The registered storage provider names are cloudflare-kv, cloudflare-r2, and memory — anything else throws at config time.

The namespaceId can also be set as kvId on the auth provider config, or as kvNamespaceId on the database config, since KV is primarily used by the auth layer. The compiler checks all of them.

Reading and Writing Values

Basic Operations

// Write a value
await env.KV.put("user:123", JSON.stringify({ name: "Alice" }));

// Read a value
const data = await env.KV.get("user:123");
const user = data ? JSON.parse(data) : null;

// Delete a value
await env.KV.delete("user:123");

// Check if key exists
const exists = await env.KV.get("user:123") !== null;

With Metadata

KV supports metadata attached to keys:

// Write with metadata
await env.KV.put("user:123", JSON.stringify(userData), {
  metadata: { updatedAt: Date.now(), version: 1 },
});

// Read with metadata
const { value, metadata } = await env.KV.getWithMetadata("user:123");

List Keys

// List all keys with prefix
const list = await env.KV.list({ prefix: "user:" });

for (const key of list.keys) {
  console.log(key.name, key.metadata);
}

// Paginate through results
let cursor = undefined;
do {
  const result = await env.KV.list({ prefix: "user:", cursor });
  // Process result.keys
  cursor = result.cursor;
} while (cursor);

Expiration and TTL

Set automatic expiration on keys:

// Expire in 1 hour (3600 seconds)
await env.KV.put("session:abc", sessionData, {
  expirationTtl: 3600,
});

// Expire at specific timestamp
await env.KV.put("session:abc", sessionData, {
  expiration: Math.floor(Date.now() / 1000) + 3600,
});

Common TTL patterns:

Use CaseTTL
Session tokens24 hours (86400)
API cache5 minutes (300)
Rate limit counters1 minute (60)
Feature flagsNo expiration

Session Cache

There is no session.storage option. Configuring a KV storage provider is the whole switch: the compiler then generates Better Auth's secondaryStorage adapter over the KV binding, and Better Auth uses it as a read-through cache in front of the auth database. Keys, values, and TTLs are Better Auth's — Quickback only supplies get/set/delete (with KV's 60-second minimum TTL applied on writes).

defineAuth("better-auth", { session: … }) accepts only expiresInDays and updateAgeInDays:

auth: defineAuth("better-auth", {
  session: { expiresInDays: 30, updateAgeInDays: 7 },
}),

Caching Patterns

Cache-Aside Pattern

async function getUser(userId: string) {
  // Check cache first
  const cached = await env.KV.get(`user:${userId}`);
  if (cached) {
    return JSON.parse(cached);
  }

  // Fetch from database
  const user = await db.select().from(users).where(eq(users.id, userId));

  // Store in cache
  await env.KV.put(`user:${userId}`, JSON.stringify(user), {
    expirationTtl: 300, // 5 minutes
  });

  return user;
}

Cache Invalidation

// Invalidate on update
async function updateUser(userId: string, data: UserUpdate) {
  await db.update(users).set(data).where(eq(users.id, userId));
  await env.KV.delete(`user:${userId}`);
}

wrangler.toml Reference

The compiler emits one namespace, bound as KV:

[[kv_namespaces]]
binding = "KV"
id = "abc123..."

With deployment environments configured, the same block is emitted once per environment as [[env.<name>.kv_namespaces]] instead.

Limitations

  • Value size - Maximum 25 MB per value
  • Key size - Maximum 512 bytes
  • Write limits - 1 write per second per key
  • Eventual consistency - Changes may take up to 60 seconds to propagate
  • Not for hot writes - Use Durable Objects for high-write scenarios

KV is optimized for read-heavy workloads. For write-heavy use cases or strong consistency, consider Durable Objects.

On this page