Valcraven Docs
Deployment & Operations

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 --remote targets 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/health response with checks.db down means the D1 binding isn't resolving — confirm database_id in wrangler.toml matches wrangler 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) uses npm run db:migrate, which runs scripts/migrate.ts against a local better-sqlite3 file (or PostgreSQL if DATABASE_URL is 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 the NEXT_TAG_CACHE_D1 binding (the same D1 instance as DB), and opennextjs-cloudflare deploy seeds 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_BUCKET binding (the same bucket as STORAGE, under an incremental-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.

On this page