Database
Creating, migrating, and wiring Cloudflare D1 with Drizzle
Last updated October 21, 2026
Data lives in Cloudflare D1 (SQLite), accessed through Drizzle ORM. The schema is in src/db/schema.ts; migrations are in src/db/migrations/.
Prerequisites
The token needs Account → D1 → Edit, see Cloudflare.
Setup
1. Create the database
pnpm exec wrangler d1 create saas-template-dbCopy the database_id into wrangler.jsonc:
{
"d1_databases": [
{
"binding": "DB",
"database_name": "saas-template-db",
"database_id": "f0aed9d8-0cca-4cb7-9ec6-be6b67ce7e69",
"migrations_dir": "./src/db/migrations"
}
]
}wrangler d1 createoffers to write the config for you. Decline and edit by hand — it serializes through JSON and strips every comment fromwrangler.jsonc.
binding is the name code reads (env.DB) and must match src/types/cloudflare.d.ts. database_name is only a label.
2. Apply migrations
Local and remote are separate:
pnpm run db:migrate:local # local dev database
pnpm run db:migrate:remote # production databaseRemote migrations ask for confirmation; non-interactive environments skip it.
Check for data before running a migration with `DROP TABLE`.0002replaces the oldapi_keystable withapikeyand drops the former. Verify it is empty first: ``bash pnpm exec wrangler d1 execute DB --remote --command "SELECT COUNT(*) FROM api_keys;"`If it is non-zero, back up first (wrangler d1 export DB --remote --output backup.sql`) before deciding.
Verify the schema:
pnpm exec wrangler d1 execute DB --remote \
--command "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"You should see ten tables: users, sessions, accounts, verifications, apikey, files, payments, subscriptions, newsletter_subscriptions, webhook_events.
3. Change the schema
Edit src/db/schema.ts, then generate a migration:
pnpm run db:generateReview the generated SQL before committing. generate reads a renamed column as "drop old + add new", which loses data. Adding a column is safer than renaming one.Usage
The Drizzle instance is created per request, never as a global singleton:
// src/db/index.ts
export function createDb(d1: D1Database) {
return drizzle(d1, { schema })
}src/auth/auth.ts calls createDb(env.DB) on each request so concurrent Workers requests never share state.
pnpm dev uses a local SQLite file under .wrangler/state, fully isolated from remote.
Commands
| Command | Purpose |
|---|---|
pnpm run db:generate | Generate a migration from schema changes |
pnpm run db:migrate:local | Apply migrations locally |
pnpm run db:migrate:remote | Apply migrations remotely |
pnpm run db:studio:local | Local Drizzle Studio |
pnpm run db:studio:remote | Remote Drizzle Studio |
pnpm exec wrangler d1 execute DB --remote --command "..." | Run arbitrary SQL remotely |
Remote Studio requiresCLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_DATABASE_ID, andCLOUDFLARE_API_TOKENin.env.