Running migrations in prod
Apply D1 migrations to production, and understand the OpenNext cache tables seeded at deploy time.
On Cloudflare D1 there is no bootstrap step — every table comes from a migration file in apps/web/migrations/, which makes those files the single source of truth for your production schema. This page covers applying them to the remote D1, how prod migrations differ from local dev, and the extra tables OpenNext manages for you.
The migration files
Migrations live in apps/web/migrations/ as numbered SQL files (000_better_auth_init.sql, 001_create_items.sql, … through the current highest number). wrangler.toml points D1 at them:
[[d1_databases]]
binding = "DB"
database_name = "valcraven-db"
database_id = "<your-d1-id>"
migrations_dir = "migrations"To author a new migration, see Database migrations. In short: npm run db:generate produces Drizzle-generated SQL from schema changes, or you hand-write a numbered .sql file — either way it lands in migrations/ and becomes part of the source of truth.
Applying to production D1
Apply pending migrations to the remote database with wrangler:
npx wrangler d1 migrations apply valcraven-db --remote- Run this whenever you add a migration and before (or as part of) the deploy that depends on it.
- Only pending migrations are applied — it's safe to re-run, and it's idempotent.
- Omitting
--remotetargets D1's local emulation, not production. For prod, always pass--remote.
The one-shot setup script (npm run deploy:cf:setup) runs this step for you, so a first-time deploy applies migrations before it ships. For routine deploys through Workers Builds, migrations are not run automatically — apply them yourself, or a deploy shipping code that expects a new column will fail at runtime until you do.
table … has no column …at runtime almost always means a migration hasn't been applied to the remote D1. Run the apply command above. Likewise, a/api/healthresponse withchecks.dbdown means the D1 binding isn't resolving — confirmdatabase_idinwrangler.tomlmatcheswrangler d1 list.
Prod (D1) vs. local dev
Production D1 and local development use different migration paths — don't confuse them:
- Local dev (Docker /
npm run dev) usesnpm run db:migrate, which runsscripts/migrate.tsagainst a localbetter-sqlite3file (or PostgreSQL ifDATABASE_URLis set). That script explicitly does not touch D1. - Production D1 is migrated only via
wrangler d1 migrations apply … --remote.
They read the same migrations/ directory, so the schema stays consistent — but the command you run differs by target.
OpenNext cache tables (seeded at deploy)
Two OpenNext caching layers ride on your existing D1 and R2, and you do not manage their schema with the app migrations above:
- Tag cache (D1) — backs Next.js
revalidateTag()/revalidatePath(). It uses theNEXT_TAG_CACHE_D1binding (the same D1 instance asDB), andopennextjs-cloudflare deployseeds and updates its tag table at deploy time. This is part of the normal deploy — there's no separate migration file for it. - Incremental cache (R2) — persists rendered ISR pages and the fetch data cache across isolate recycles and deploys, via the
NEXT_INC_CACHE_R2_BUCKETbinding (the same bucket asSTORAGE, under anincremental-cache/key prefix). No schema to migrate — it's object storage.
Both are configured in apps/web/open-next.config.ts. See Caching for how the app uses them.
Realtime Worker migrations
The realtime Worker (apps/realtime) has its own migrations for its Durable Object SQLite classes, declared as [[migrations]] tags in apps/realtime/wrangler.toml (v1 creates the AppAgent class, v2 the RealtimeChannel class). These apply automatically on wrangler deploy and are idempotent — wrangler skips already-applied tags. You don't run a separate command; deploying the realtime Worker applies them. Never edit an existing migration tag — add a new one for a new DO class.