# 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 Case | Description |
|----------|-------------|
| **Session cache** | Wired as Better Auth's `secondaryStorage` — a read-through cache in front of the auth database |
| **JWT revocation** | The store behind `auth.jwt.revocationCheck: 'kv'` |
| **Cache** | Cache expensive database queries or API responses |
| **Rate Limiting** | Track request counts per user/IP |
| **Feature Flags** | Store configuration that changes infrequently |

## Namespace Setup

Create a KV namespace via Wrangler:

```bash
# 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:

```typescript
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

```ts
// 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:

```ts
// 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

```ts
// 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:

```ts
// 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 Case | TTL |
|----------|-----|
| Session tokens | 24 hours (86400) |
| API cache | 5 minutes (300) |
| Rate limit counters | 1 minute (60) |
| Feature flags | No 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`:

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

## Caching Patterns

### Cache-Aside Pattern

```ts
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

```ts
// 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`:

```toml
[[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](/platform/realtime/durable-objects).
