Deno KV is a globally replicated key-value database built directly into the Deno runtime — no external service or driver needed. await Deno.openKv() opens a database in development, automatically backed by FoundationDB on Deno Deploy for production global replication. Keys are tuples of strings and numbers. Atomic transactions check-and-set multiple keys consistently. kv.list() scans key ranges with prefix filters. kv.enqueue() pushes durable messages consumed by kv.listenQueue(). kv.watch() streams value updates to connected clients for real-time data. Deno Deploy runs Deno KV at the edge across 35 global regions — reads are strongly consistent within a region. Claude Code generates Deno KV data models, atomic transaction patterns, queue workers, watch-based subscriptions, and secondary index patterns for production Deno applications.
CLAUDE.md for Deno KV Projects
## Deno KV Stack
- Runtime: Deno >= 1.45 — Deno KV is stable
- Open: const kv = await Deno.openKv() — local SQLite in dev, FoundationDB on Deploy
- Keys: Deno.KvKey — tuple of strings/numbers: ["orders", userId, orderId]
- Atomic: kv.atomic().check(...).set(...).delete(...).commit() — all-or-nothing
- List: kv.list({ prefix: ["orders", userId] }) — returns AsyncIterator
- Queue: kv.enqueue(payload, { delay: 0 }) + kv.listenQueue(handler)
- Watch: kv.watch([key]) — AsyncIterator of value change events
- Test: Deno.openKv(":memory:") — in-memory for unit tests
Data Models and CRUD
// lib/orders.ts — Deno KV data access layer
const kv = await Deno.openKv()
interface Order {
id: string
customerId: string
status: "pending" | "processing" | "shipped" | "delivered" | "cancelled"
totalCents: number
items: OrderItem[]
createdAt: string
updatedAt: string
}
interface OrderItem {
productId: string
name: string
quantity: number
priceCents: number
}
// Key structure:
// ["orders", orderId] — primary key
// ["orders_by_customer", customerId, createdAt, orderId] — secondary index
// ["orders_by_status", status, orderId] — secondary index
export async function createOrder(
customerId: string,
items: OrderItem[]
): Promise<Order> {
const id = crypto.randomUUID()
const now = new Date().toISOString()
const totalCents = items.reduce(
(sum, item) => sum + item.priceCents * item.quantity,
0
)
const order: Order = {
id,
customerId,
status: "pending",
totalCents,
items,
createdAt: now,
updatedAt: now,
}
// Atomic: write primary + both secondary indexes together
const result = await kv.atomic()
.set(["orders", id], order)
.set(["orders_by_customer", customerId, now, id], { id, totalCents, status: "pending" })
.set(["orders_by_status", "pending", id], { id, customerId, totalCents })
.commit()
if (!result.ok) throw new Error("Failed to create order — transaction conflict")
return order
}
export async function getOrder(orderId: string): Promise<Order | null> {
const entry = await kv.get<Order>(["orders", orderId])
return entry.value
}
export async function updateOrderStatus(
orderId: string,
newStatus: Order["status"]
): Promise<Order> {
// Read-modify-write with optimistic concurrency
while (true) {
const entry = await kv.get<Order>(["orders", orderId])
if (!entry.value) throw new Error(`Order ${orderId} not found`)
const oldStatus = entry.value.status
const updated: Order = {
...entry.value,
status: newStatus,
updatedAt: new Date().toISOString(),
}
const result = await kv.atomic()
// Check version hasn't changed since we read
.check(entry)
.set(["orders", orderId], updated)
// Update secondary indexes
.delete(["orders_by_status", oldStatus, orderId])
.set(["orders_by_status", newStatus, orderId], {
id: orderId,
customerId: updated.customerId,
totalCents: updated.totalCents,
})
.commit()
if (result.ok) return updated
// Retry on conflict (another write happened between read and write)
}
}
export async function listCustomerOrders(
customerId: string,
limit = 20
): Promise<Order[]> {
const orders: Order[] = []
// Scan secondary index prefix
const entries = kv.list<{ id: string }>(
{ prefix: ["orders_by_customer", customerId] },
{ limit, reverse: true } // Newest first
)
for await (const entry of entries) {
const order = await getOrder(entry.value.id)
if (order) orders.push(order)
}
return orders
}
KV Queue
// lib/queue.ts — Deno KV queue for background work
interface FulfillmentJob {
type: "fulfillment"
orderId: string
customerId: string
}
interface EmailJob {
type: "email"
to: string
template: string
vars: Record<string, unknown>
}
type QueueJob = FulfillmentJob | EmailJob
export async function enqueueOrderFulfillment(
orderId: string,
customerId: string
): Promise<void> {
await kv.enqueue(
{ type: "fulfillment", orderId, customerId } satisfies FulfillmentJob,
{ delay: 0 }
)
}
export async function enqueueEmail(
to: string,
template: string,
vars: Record<string, unknown>
): Promise<void> {
await kv.enqueue(
{ type: "email", to, template, vars } satisfies EmailJob,
{ delay: 0 }
)
}
// Worker: call once at startup — processes messages indefinitely
export function startQueueWorker(): void {
kv.listenQueue(async (job: QueueJob) => {
console.log("Processing job", job.type)
if (job.type === "fulfillment") {
await processFulfillment(job.orderId, job.customerId)
} else if (job.type === "email") {
await sendEmail(job.to, job.template, job.vars)
}
})
console.log("Queue worker started")
}
async function processFulfillment(
orderId: string,
customerId: string
): Promise<void> {
await updateOrderStatus(orderId, "processing")
const result = await fetch("https://fulfillment.api.com/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orderId, customerId }),
})
if (!result.ok) throw new Error(`Fulfillment API error: ${result.status}`)
const { fulfillmentId } = await result.json()
await updateOrderStatus(orderId, "fulfilled")
await enqueueEmail(customerId, "order-fulfilled", { orderId, fulfillmentId })
}
Watch for Real-Time Updates
// routes/orders/[id]/stream.ts — Server-Sent Events with kv.watch
import { Hono } from "hono"
const app = new Hono()
app.get("/orders/:id/stream", async (c) => {
const orderId = c.req.param("id")
// Verify order exists
const order = await getOrder(orderId)
if (!order) return c.json({ error: "Not found" }, 404)
// Stream SSE to client
const stream = new ReadableStream({
async start(controller) {
const watcher = kv.watch<Order>([["orders", orderId]])
for await (const [entry] of watcher) {
if (entry.value === null) {
controller.close()
break
}
const data = JSON.stringify({
id: entry.value.id,
status: entry.value.status,
updatedAt: entry.value.updatedAt,
})
controller.enqueue(`data: ${data}\n\n`)
// Close stream when order reaches terminal state
if (["delivered", "cancelled"].includes(entry.value.status)) {
controller.enqueue(`event: done\ndata: {}\n\n`)
controller.close()
break
}
}
},
})
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
})
})
Testing with In-Memory KV
// tests/orders.test.ts — deterministic unit tests with in-memory KV
import { assertEquals, assertRejects } from "@std/assert"
// Override with in-memory database for tests
Deno.test("createOrder stores order with correct structure", async () => {
// Swap global kv for in-memory instance
const testKv = await Deno.openKv(":memory:")
const items = [
{ productId: "prod-1", name: "Widget", quantity: 2, priceCents: 999 },
]
const order = await createOrderWithKv(testKv, "cust-123", items)
assertEquals(order.customerId, "cust-123")
assertEquals(order.status, "pending")
assertEquals(order.totalCents, 1998)
assertEquals(order.items.length, 1)
// Verify it's stored
const stored = await testKv.get<Order>(["orders", order.id])
assertEquals(stored.value?.id, order.id)
testKv.close()
})
Deno.test("updateOrderStatus handles concurrent writes correctly", async () => {
const testKv = await Deno.openKv(":memory:")
const order = await createOrderWithKv(testKv, "cust-123", [...])
// Simulate two concurrent updates
const result = await Promise.all([
updateOrderStatusWithKv(testKv, order.id, "processing"),
updateOrderStatusWithKv(testKv, order.id, "cancelled"),
])
// Exactly one should succeed
const final = await testKv.get<Order>(["orders", order.id])
const validStatuses = new Set(["processing", "cancelled"])
assertEquals(validStatuses.has(final.value!.status), true)
testKv.close()
})
For the Upstash Redis serverless alternative that provides a similar HTTP-based KV API working in edge environments with pub/sub and sorted sets beyond basic KV, see the Redis guide for caching and queue patterns. For Cloudflare Workers KV when deploying on Cloudflare infrastructure with globally distributed eventually-consistent reads at low latency, the Cloudflare Workers guide covers KV namespace bindings. The Claude Skills 360 bundle includes Deno KV skill sets covering atomic transactions, queue workers, and watch subscriptions. Start with the free tier to try Deno KV data model generation.