Skip to content

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 (use BEGIN TRANSACTION; / COMMIT; inside one file if you need atomicity).
  • Tracking table: wrangler records applied migrations in d1_migrations by default. Override per database with migrationsTable (useful when two D1s share a migrationsDir, or to avoid clashing with your app's own table named d1_migrations).
ts
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 kindDeclared onMigrated by
Stack single D1resources.d1[] (type: "single")tamer migrate
Stack shard groupresources.shardGroups[] ({ name, binding, migrationsDir })tamer migrate (every physical shard NNN)
Per-tenant utility D1wfp.namespaces.<product>.workers.<svc>.d1[]tamer wfp tenant migrate
Tenant shard groupwfp.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

bash
tamer migrate --env dev
tamer migrate --env dev --worker api   # one worker only

For each worker (then each D1 / shard group with a migrationsDir):

  1. Regenerate wrangler.json (so the binding points at the right DB name).
  2. Run wrangler d1 migrations apply <derivedDbName> — once per database. Non-local envs get --remote so 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

bash
tamer wfp tenant migrate --env dev --workspace acme

Runs 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 deploy

migrate 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.

bash
# 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 d1

When 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:

  1. tamer destroy --target d1:<logical> — deletes the Cloudflare D1 and prunes its state entry (other resources untouched).
  2. tamer apply --target d1:<logical> — creates a fresh D1 with a new UUID and regenerates every worker's wrangler.json.
  3. tamer migrate — runs wrangler d1 migrations apply for the env; the fresh database replays all files in migrationsDir from scratch.
  4. 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 run tamer deploy --env <env>.
  • --confirm-env — same protected-env gate as tamer 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 reruns wrangler d1 migrations apply locally. No Cloudflare API calls. If wrangler.json is 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):

bash
# 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 localdev

Remote 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.

Released under the Tamer Evaluation License.