Cloudflare D1 is SQLite running in Cloudflare’s global network. Your Worker runs in 300 edge locations, and D1 provides a database co-located in the same datacenter — eliminating the round-trip to a central database. D1 handles migrations, replication, and point-in-time recovery. The API is the familiar SQLite API: db.prepare(), db.run(), and db.batch() for transactions. Claude Code writes D1-backed Workers, schema migrations, Drizzle ORM integration, and the patterns that handle D1’s consistency model correctly.
CLAUDE.md for Cloudflare D1 Projects
## Edge Stack
- Cloudflare Workers + D1 (SQLite at edge)
- ORM: Drizzle ORM with drizzle-orm/d1 adapter
- Migrations: drizzle-kit generate → apply via wrangler d1 migrations apply
- Binding: DB (D1Database) in wrangler.toml and worker env
- Consistency: D1 is eventually consistent for read replicas; primary write is strong
- Batch: use db.batch([...]) for multiple statements in one round-trip
- Development: wrangler dev with --local for SQLite in .wrangler/state/
Wrangler Configuration
# wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2026-09-01"
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
[[env.production.d1_databases]]
binding = "DB"
database_name = "my-app-db-production"
database_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
migrations_dir = "drizzle"
Schema and Migrations
// src/db/schema.ts — Drizzle schema for D1 (SQLite dialect)
import { sqliteTable, text, integer, blob } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
export const orders = sqliteTable('orders', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
userId: text('user_id').notNull(),
status: text('status', { enum: ['pending', 'shipped', 'delivered', 'cancelled'] })
.notNull()
.default('pending'),
totalCents: integer('total_cents').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
metadata: text('metadata', { mode: 'json' }).$type<Record<string, unknown>>(),
});
export const orderItems = sqliteTable('order_items', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
orderId: text('order_id').notNull().references(() => orders.id, { onDelete: 'cascade' }),
productId: text('product_id').notNull(),
quantity: integer('quantity').notNull(),
priceCents: integer('price_cents').notNull(),
productName: text('product_name').notNull(),
});
# Generate migration SQL from schema
npx drizzle-kit generate
# Apply migration to local dev D1
npx wrangler d1 migrations apply my-app-db --local
# Apply to production
npx wrangler d1 migrations apply my-app-db --remote
Worker with D1 Queries
// src/index.ts — Cloudflare Worker with D1
import { drizzle } from 'drizzle-orm/d1';
import { eq, desc, and } from 'drizzle-orm';
import * as schema from './db/schema';
export interface Env {
DB: D1Database;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const db = drizzle(env.DB, { schema });
// Route: GET /orders/:id
if (request.method === 'GET' && url.pathname.match(/^\/orders\/[\w-]+$/)) {
const orderId = url.pathname.split('/')[2];
const order = await db.query.orders.findFirst({
where: eq(schema.orders.id, orderId),
with: { items: true },
});
if (!order) return new Response('Not found', { status: 404 });
return Response.json(order, {
headers: { 'Cache-Control': 'private, max-age=30' },
});
}
// Route: POST /orders
if (request.method === 'POST' && url.pathname === '/orders') {
const body = await request.json() as { userId: string; items: any[] };
const totalCents = body.items.reduce(
(sum: number, i: any) => sum + i.quantity * i.priceCents, 0
);
// D1 transaction: create order + items atomically
const result = await db.batch([
db.insert(schema.orders).values({
userId: body.userId,
totalCents,
}).returning(),
...body.items.map((item: any) =>
db.insert(schema.orderItems).values({
orderId: 'PLACEHOLDER', // Set in transaction result
productId: item.productId,
quantity: item.quantity,
priceCents: item.priceCents,
productName: item.productName,
})
),
]);
return Response.json(result[0][0], { status: 201 });
}
// Route: GET /orders?userId=...
if (request.method === 'GET' && url.pathname === '/orders') {
const userId = url.searchParams.get('userId');
const userOrders = await db.query.orders.findMany({
where: userId ? eq(schema.orders.userId, userId) : undefined,
orderBy: desc(schema.orders.createdAt),
limit: 20,
with: { items: true },
});
return Response.json(userOrders);
}
return new Response('Not found', { status: 404 });
},
};
D1 Batch Operations
// Batch: multiple statements, one round-trip to D1
async function batchOrderOps(env: Env, orderId: string, newStatus: string) {
const results = await env.DB.batch([
// Update order status
env.DB.prepare(
'UPDATE orders SET status = ?, updated_at = ? WHERE id = ?'
).bind(newStatus, Date.now(), orderId),
// Log the status change
env.DB.prepare(
'INSERT INTO order_events (order_id, event_type, created_at) VALUES (?, ?, ?)'
).bind(orderId, `status.${newStatus}`, Date.now()),
// Return updated order
env.DB.prepare('SELECT * FROM orders WHERE id = ?').bind(orderId),
]);
return results[2].results[0]; // The SELECT result
}
For the full Cloudflare Workers runtime patterns beyond D1, the Cloudflare Workers guide covers KV, R2, Queues, and Durable Objects. For the Drizzle ORM patterns that also apply to D1’s SQLite dialect, the Drizzle ORM guide covers schema definition and relational query patterns. The Claude Skills 360 bundle includes Cloudflare D1 skill sets covering schema migrations, Drizzle integration, and edge-optimized query patterns. Start with the free tier to try D1 Worker generation.