D1 Migrations
How Tamer runs D1 schema migrations, and how migration files are authored. Tamer does not have its own migration format — it wraps wrangler d1 migrations so any wrangler-compatible migration setup works.
Migration files
Each D1 / shard group / per-tenant utility database that should be migrated declares a migrationsDir pointing at a directory of SQL files:
db/primary/migrations/
0001_init.sql
0002_add_users_index.sql
0003_create_events_table.sql- Naming: wrangler sorts files lexically — prefix with a zero-padded sequence (
0001_,0002_, …) so they apply in order. - Format: plain
.sql. Statements are batched per file. Avoid statement-level delimiters wrangler can't split (useBEGIN TRANSACTION;/COMMIT;inside one file if you need atomicity). - Tracking table: wrangler records applied migrations in
d1_migrationsby default. Override per database withmigrationsTable(useful when two D1s share amigrationsDir, or to avoid clashing with your app's own table namedd1_migrations).
workers: {
api: {
resources: {
d1: [{
logicalName: "app-db",
type: "single",
binding: "APP_DB",
migrationsDir: "db/app/migrations",
// migrationsTable: "tamer_migrations", // optional override
}],
},
},
},Where migrations run
| D1 kind | Declared on | Migrated by |
|---|---|---|
| Stack single D1 | resources.d1[] (type: "single") | tamer migrate |
| Stack shard group | resources.shardGroups[] ({ name, binding, migrationsDir }) | tamer migrate (every physical shard NNN) |
| Per-tenant utility D1 | wfp.namespaces.<product>.workers.<svc>.d1[] | tamer wfp tenant migrate |
| Tenant shard group | wfp.namespaces.<product>.workers.<svc>.shardGroups[] | tamer wfp tenant migrate |
Stack-level and tenant-level migrations are intentionally separate: stack D1s are created on tamer apply, tenant shards are created on tamer wfp tenant provision. Migrations follow the same split.
tamer migrate
tamer migrate --env dev
tamer migrate --env dev --worker api # one worker onlyFor each worker (then each D1 / shard group with a migrationsDir):
- Regenerate
wrangler.json(so the binding points at the right DB name). - Run
wrangler d1 migrations apply <derivedDbName>— once per database. Non-localenvs get--remoteso migrations target the live D1 instead of the local SQLite cache.
tamer migrate resolves the derived database name per env (single) or per shard (sharded — one apply per physical shard recorded in state).
tamer wfp tenant migrate
tamer wfp tenant migrate --env dev --workspace acmeRuns wrangler d1 migrations apply against every physical shard in each of the tenant's shard groups, plus each per-tenant utility D1. Use this after wfp tenant provision and whenever you ship a new migration file.
Local
tamer migrate without --remote targets wrangler's local SQLite cache — no Cloudflare API calls. Useful with tamer dev for iterating on schema before hitting a real D1. See Local Development.
CI ordering
tamer apply → tamer migrate → tamer deploymigrate must run after apply (resources must exist) and before deploy (the worker code may depend on the new schema). For PR-preview ephemeral envs, run migrate for stack-level D1s too — tenant shards are migrated during wfp tenant provision. See CI Workflows.
Squashing migrations (greenfield reset)
While you're still greenfield in a lower env (typically dev), you may want to squash many migration files into one clean 0001_init.sql and wipe the dev database so wrangler replays the full history from scratch. Tamer does not delete your migration files — squashing the SQL on disk is manual.
# 1. Squash: replace migrations/*.sql with a single 0001_init.sql (manual)
# 2. Reset one D1 (destroy → apply → migrate → redeploy bound workers):
tamer reset --env dev --target d1:app-db
# …or every declared stack D1 at once:
tamer reset --env dev --kind d1When resetting multiple D1s (--kind d1), Tamer destroys each database targeted, then runs one full tamer apply (not scoped per logical name) so wrangler generation sees every recreated binding at once.
What tamer reset does, in order:
tamer destroy --target d1:<logical>— deletes the Cloudflare D1 and prunes its state entry (other resources untouched).tamer apply --target d1:<logical>— creates a fresh D1 with a new UUID and regenerates every worker'swrangler.json.tamer migrate— runswrangler d1 migrations applyfor the env; the fresh database replays all files inmigrationsDirfrom scratch.tamer deploy— redeploys only workers that declare the reset D1. Required because deployed workers still bind the old database id until redeployed.
Flags:
--skip-deploy— stops after migrate; prints a warning that workers still bind the deleted UUID until you runtamer deploy --env <env>.--confirm-env— same protected-env gate astamer destroy(default gate:prod/production).--env local(stack) — deletes miniflare D1 SQLite under the worker's.wrangler/state/v3/d1(matching wrangler 4.x layout) and rerunswrangler d1 migrations applylocally. No Cloudflare API calls. Ifwrangler.jsonis missing, a temporary config is written using the derived database name as the local persistence id.
Stack-level shard groups are out of scope for tamer reset — use tamer destroy --target + apply for those.
WFP tenant D1s use a separate command (same greenfield squash workflow):
# All tenant D1s for one workspace (utility + shard groups):
bunx tamer wfp tenant reset --env dev --workspace acme --kind d1
# One utility database:
bunx tamer wfp tenant reset --env dev --workspace acme --target d1:system
# One shard group (every physical shard index in state):
bunx tamer wfp tenant reset --env dev --workspace acme --target shard_group:primary
# Local miniflare (template worker dir; no CF API / state / reupload):
bunx tamer wfp tenant reset --env local --workspace localdev --kind d1
bunx tamer wfp tenant migrate --env local --workspace localdev
bunx tamer wfp tenant seed --env local --workspace localdevRemote tenant reset deletes and recreates the Cloudflare D1(s), updates tenant state, runs migrations, and reuploads dispatch scripts (--skip-reupload to opt out). It does not remove the tenant record or dispatch namespace (unlike wfp tenant destroy). Local tenant reset only wipes miniflare SQLite and replays migrations + seed — see Local Development. See Single product, multi-tenant.
Related docs
- Seed — reference/default data (
seedDir), separate from migrations - Lifecycle — apply / migrate / deploy in the stack lifecycle
- Local Development — local SQLite migration loop
- Naming Conventions — derived D1 names