One Claude Code Tip a Day: Make Migrations Boring
Use Claude Code as a migration safety partner: inspect the current schema first, demand a boring migration plan, run dry checks, verify app code, and write the rollback note before deployment.
This post is part of the “One Claude Code Tip a Day” series — a daily guide to using Claude Code more effectively.
The failure: a migration that looked too clever
Database work is where Claude Code can be both extremely useful and quietly dangerous. The bad session starts innocently: you need to add a `billing_status` column, backfill old subscriptions, and update one API response. Claude reads a model file, writes a migration, updates TypeScript types, and confidently says the change is complete. The migration even passes on a fresh local database.
Then staging fails because production has old rows with `NULL` customer IDs. Or the down migration drops a column before removing an index. Or a background worker reads the new enum before the app deploy has finished. The problem is not that Claude touched SQL. The problem is that you let it make a database change like a normal code edit. Today’s habit: make migrations boring before you let Claude make them real.
Start with schema evidence, not a patch
Begin with a read-only prompt. The goal is to force Claude Code to inspect the actual migration history, schema conventions, and callers before it writes anything:
Prompt: `Read db/migrations, schema.prisma, src/billing/subscription.ts, and the tests that create subscriptions. Do not edit. Summarize the current subscription schema, the migration style used in this repo, rows that may already exist in production, and the commands that can validate a migration locally.`
A useful answer should cite files, not vibes. It should notice whether the project uses Prisma, Knex, Rails migrations, raw SQL, or a hosted migration tool. It should name default values, nullable columns, enum patterns, and seed/test factories. If Claude cannot tell you how this repo normally migrates data, it is not ready to write a migration.
Ask for the boring migration plan
Once the evidence is visible, ask for a plan that avoids cleverness:
Prompt: `Propose the smallest safe migration for adding billing_status. Prefer additive changes, explicit defaults, and reversible steps. Separate schema change, data backfill, application code update, and verification commands. Do not edit yet.`
This prompt matters because Claude often wants to collapse everything into one elegant patch. For migrations, elegance is overrated. Boring means additive first. Boring means old app code can survive the new schema for a deploy window. Boring means the backfill is explicit and testable. Boring means the rollback note says what data might not be recoverable.
Let Claude edit only inside the plan
When the plan is acceptable, give Claude narrow boundaries:
Prompt: `Implement only the additive migration and the minimal application changes needed to read billing_status safely. Do not remove old fields. Do not change unrelated billing behavior. Add or update tests that create old-style subscription records and prove the new code handles them.`
Then run proof commands through Claude Code instead of accepting the written explanation: `!pnpm test billing`, `!pnpm prisma migrate diff --from-schema-datamodel schema.prisma --to-migrations db/migrations`, `!pnpm typecheck`, or whatever fits the repo. The exact commands matter less than the rule: every migration needs a command that exercises old data and a command that checks the new schema.
Second pass: review deploy order and rollback
The second pass is where this habit earns its keep. Ask Claude to review the migration as an operator, not as the author:
Prompt: `Review this diff for deployment risk. What happens if the migration runs before the app deploy? What happens if the app deploy runs before the migration? What happens to existing rows? What is the rollback path? Mark any file that is not required for this migration.`
This catches subtle failures. Maybe the API response now assumes `billing_status` is always present, but one environment runs app code before migrations. Maybe the worker should tolerate missing values for one release. Maybe the test factory creates only new rows and never simulates production data. Claude can find these issues, but only if you ask it to think in deploy order instead of code order.
Failure modes to watch for
First, do not let Claude invent production facts. If it says `all rows have customer_id`, ask for the query, fixture, or migration that proves it. If the fact is not in the repo, mark it as an assumption.
Second, do not accept a migration that only works on an empty database. Fresh-database success is necessary, not sufficient. Ask for a test or local seed path that represents old data.
Third, do not mix cleanup with migration. Renaming helpers, deleting deprecated fields, and reformatting billing modules may be good work, but not in the same patch as a schema change. Keep the blast radius reviewable.
Rule of thumb
Use Claude Code to make database changes slower at the beginning and safer at the end. Before editing, make it read the schema history. Before running, make it state the deploy order. Before accepting, make it prove old data still works and write the rollback note. A good migration session should feel almost dull: small diff, explicit commands, boring defaults, clear rollback. If the migration feels clever, pause and make Claude make it boring.