Most pages ranking for this phrase are about moving a database to a new system. An AI-built app has a different problem: a schema built by clicking in the Supabase table editor, with nothing in the repository. Its database migration checklist has 6 lines: baseline the live schema, stop dashboard edits, write every change as a file, apply the files to a clean database, compare, and plan the undo.
The database migration checklist: six lines for a schema that grew up in a dashboard
A database migration checklist for an app with live data has 6 lines: baseline the live schema, stop dashboard edits, write each change as a new file, apply the whole sequence to a clean database, compare the result with production, and state the undo before each change runs.
Untracked dashboard changes make environments difficult to reproduce and maintain. In the data consistency checklist for SaaS, this is the sixth of the database checks: a staging copy, a new environment and a new developer’s laptop all need a schema that files can rebuild.
The table gives each line, what it leaves you with, and the test I use to call it done. The last column is my working rule, not a published standard.
| Checklist line | What it produces | How you know it is done |
|---|---|---|
| 1. Baseline | The live production schema, captured as the first migration file | The file is in the repository and the tool records it as applied in production |
| 2. Freeze | Nobody, and no AI builder, changes the schema outside a file | Dashboard users and agent database roles cannot change the schema, or a written rule says so where the platform cannot enforce it |
| 3. Every change is a new file | One new migration per change; a file that has run is never edited | Every schema change merged this month has its own file |
| 4. Clean apply | The whole sequence runs on an empty database | One run from the first file to the last, with no manual step |
| 5. Compare | The clean result matches production, and staging matches both | An empty diff, saved with the date |
| 6. Undo | Each migration states its undo before it runs | The pull request says how the change is reversed or why it fixes forward, and names the fallback backup |
The database migration best practices I hold to in production are few, and they are my working rules rather than anyone’s standard: one change per file, reviewed in a pull request, applied by the pipeline and never from a laptop, a backup before anything destructive, and long-running changes scheduled off peak. The review gate is GitHub branch protection, with a plan condition on private repositories: protected branches are “available in public repositories with GitHub Free and GitHub Free for organizations” and “in public and private repositories with GitHub Pro, GitHub Team, GitHub Enterprise Cloud, and GitHub Enterprise Server”. On a private repository on GitHub Free, the review is a team rule, not a setting.
The deployment half of a database migration deployment checklist, meaning the order of code and schema, backward-compatible changes and zero downtime, belongs to how to run database migrations on deploy.
What are database migrations: versioned files and the table that records them
Database migrations are versioned files that change a database’s schema, applied in order, with a table in the database recording which files have already run. Prisma Migrate, Rails and Flyway each keep that record, which is how the tool knows the next file to apply.
The record has a name in every tool. Prisma ORM 7 writes to _prisma_migrations, Rails to schema_migrations, Drizzle Kit to __drizzle_migrations in the drizzle schema, Liquibase to DATABASECHANGELOG, and Flyway adds what its docs call a schema history table. Supabase keeps the same kind of record in supabase_migrations.schema_migrations. Because the table says a file ran, the file must never change afterwards: an edited file no longer describes what happened on any database it already touched. A migrations folder that nothing applies is just a folder, not a record.
The other meaning: a checklist for data migration to a new system, and how to test it
A data migration checklist, in the other sense, covers moving a whole database to a new system. It has 5 lines: inventory what moves, rehearse on a copy, compare row counts and checksums with that copy table by table, cut over in a quiet window with writes stopped, and keep the old system untouched until the new one is proven.
Most search results for this phrase mean that move: a whole database going to a new host or product. That is a project with a start and an end, not a habit in a repository. For a small app, the five lines are my working rules:
- Inventory what moves: tables, files in storage, auth users, secrets and scheduled jobs.
- Rehearse the move on a copy of production, and time it.
- Compare row counts and checksums per table between the copy the move started from and the new database, then run the app’s smoke tests against the new database.
- Switch the app to the new database at a quiet time, after stopping writes.
- Keep the old system untouched and reachable until the new one has run for an agreed period.
The third line is the data migration testing checklist, and my working rule for it concerns the source: compare against the frozen copy the move started from, never against a live database the app is still writing to, because that one never matches. When the move is off an AI builder, the things that break are covered in what breaks when you migrate off Lovable, Base44, Replit or Bolt.
For the Production Hardening Sprint, we work inside the existing codebase, and the app’s current framework and hosting setup are our starting point.
What goes wrong without it
Schema changes made in the dashboard are not tracked anywhere except the live database, and most of the failures below start there. Picture each row happening to an app that has run for a few months on table-editor changes:
| What you see | What happened | The checklist line that prevents it |
|---|---|---|
| The next developer’s local app fails on a query nobody can explain | A column was added in the table editor; it exists in production and nowhere else | 1. Baseline, 2. Freeze |
| A release passes in staging and fails live | Staging was cloned months ago and production kept changing | 5. Compare |
| A column appears that no file mentions | An agent with database access ran an ALTER to make its feature work, and said so in a chat nobody reread | 2. Freeze |
| A disaster recovery test, a new environment and a due diligence request all stall at the same point | No file describes the database, so nothing can rebuild it | 1. Baseline, 4. Clean apply |
| The file in the repository no longer matches what ran | An applied migration was edited. Flyway’s validate fails on a checksum difference and Liquibase’s validate checks for checksum errors; for any other tool, check its docs | 3. Every change is a new file |
In the second row, the staging database schema differs from production because staging was copied once and production went on changing. Keeping staging in step is its own job, covered in one database and no staging. None of the five rows is a verdict on your database; the clean apply under How to verify it is what decides.
One app I audited, an AI coding workspace, defined its schema in two places that disagreed: one copy of a save function was a plain insert and the other an upsert, and a status column was an enum in one and text with a check in the other. One migration file’s own header said it was superseded. My reading: when a schema is written down in two places, neither one is the record, and applying the files to an empty database is how you find out which one production matches.
How to do it: the baseline, the tools, data changes, and the undo
The work is four jobs: capture what exists, pick the tool that will own it, keep data changes out of schema files, and write the undo before each change.
Start in the browser. Supabase’s local development post from August 2023 says it added a Migrations view to the dashboard to track migration history. Supabase tracks applied migrations in a table called supabase_migrations.schema_migrations, so select * from supabase_migrations.schema_migrations; in the SQL editor reads it without changing anything. From a terminal, in a project linked with supabase link, supabase migration list lists migration history in both local and remote databases. The CLI creates that table the first time migrations are pushed, so if it is missing or empty and the app has tables, every change so far was untracked.
How to move schema changes into migrations: the baseline, then the freeze
Moving schema changes into migrations starts with a baseline: a dump of the live production schema, committed as the first migration and marked as already applied in production. From then on every change is a new file, reviewed in a pull request and applied by the pipeline, and the table editor is used to look, never to change.
- 01 Back up production before you touch anything.
- 02 Dump the live schema, structure only, limited to the app's own schemas:
pg_dump --schema-onlywith one--schemaper schema. - 03 Turn the dump into the first migration with the tool's own baseline command.
- 04 Mark that migration as already applied in production, so the tool never tries to run it there.
- 05 Apply it to a fresh local database and to staging.
- 06 Freeze: remove schema-changing rights from dashboard users and from any AI agent's database role, or agree the rule in writing where the platform cannot enforce it.
- 07 Make the next change file number two, through a pull request.
Step 1 is the database backup checklist for startups. Step 2 uses the flags in pg_dump’s documentation: --schema-only dumps “only the object definitions (schema), not data or statistics”. What a reviewer should read in that file is the database review checklist. Two cautions on the file itself. pg_dump says that with -n it “makes no attempt to dump any other database objects that the selected schema(s) might depend upon”, so an extension or a function that lives in another schema has to go into the baseline as its own statement. And a plain dump is written to be fed to psql, so psql-only lines such as \restrict do not belong in a file a migration tool runs (my reading).
Step 6 depends on the platform. On Supabase, the dashboard role that cannot change the project, Read-Only, “is only available on the Team and Enterprise plans”, and a Developer has “content access to project resources”, which I read as enough to edit tables. On the Free and Pro plans, the freeze is therefore a written rule plus Supabase’s own: “never change the remote database directly”.
Steps 3 and 4 differ by tool. Each cell below comes from the tool’s own docs, read on the date in the method note at the end of this section:
| Tool | Baseline command | How to mark it applied in production |
|---|---|---|
| Prisma (ORM 7 docs) | prisma db pull to bring the Prisma schema in sync, then prisma migrate diff --from-empty --to-schema prisma/schema.prisma --script into a 0_-prefixed folder such as 0_init | prisma migrate resolve --applied 0_init, which adds it to _prisma_migrations as applied |
| Drizzle Kit | drizzle-kit pull introspects the database into a Drizzle schema file | drizzle-kit pull --init, which marks the pulled schema as an applied migration in the database |
| Atlas | atlas migrate diff my_baseline --dir "file://migrations" --dev-url <dev database URL> --to <database URL> writes a file reflecting the current schema | atlas migrate apply --baseline <version> on the first run; Atlas marks that version as already applied |
| Flyway | A baseline script that captures the existing state of production, per Redgate’s baselines guide | flyway baseline (or migrate with -baselineOnMigrate), which creates the schema history table |
| Liquibase (5.1 docs) | generate-changelog; row-level security policies are not among its valid diff types, and functions, triggers and check constraints need Liquibase Secure | changelog-sync, which marks all undeployed changes as executed in DATABASECHANGELOG |
Prisma ORM 8, the version its docs now default to, replaces the Prisma row with prisma db sign, which checks that the tables match the Prisma contract and then writes the marker that records the database’s state. The walk-throughs are Prisma’s baselining guide, Drizzle Kit’s documentation, Flyway’s baseline command and Liquibase’s changelog-sync command. Supabase’s CLI has its own pull-then-diff path, walked through in the Supabase staging environment retrofit.
Whatever the tool, read the baseline file before you commit it. My working rule is to check it for indexes, constraints, row-level security policies, functions, triggers, extensions and grants. Then confirm the dump was not taken with a flag that leaves things out: --no-owner drops the ownership commands, --no-privileges drops grant and revoke commands, and --no-policies drops row security policies. Every command and flag above comes from the vendors’ documentation as read on 28 September 2026; none was run for this page.
Migration tools: Atlas schema migration, Prisma Migrate, Liquibase and the dashboard you are leaving
Migration tools follow 2 main models. Change-based tools such as Prisma Migrate, Drizzle Kit, Flyway, Liquibase and Rails apply an ordered list of files. Atlas also offers a declarative workflow: it compares the database with the desired schema and plans the migration. By my working rule, the safe choice is usually the tool your framework already ships.
Prisma’s Data Guide calls the two models “state based migrations and change based migrations”. Atlas schema migration sits on both sides: Atlas’s documentation describes a versioned workflow built on a migration directory and a declarative one that “generates and executes a migration plan” from the difference between the database and the schema you describe. The license column matters if you want open source migration tools, and it prints only what each project’s own pages say. The last column is my reading, not a ranking; no tool here was tested for this page.
| Tool | Model | Baseline support | License and paid features | Fits when |
|---|---|---|---|---|
| Prisma Migrate (ORM 7) | Change-based; applied files recorded in _prisma_migrations | Yes: a 0_init baseline plus migrate resolve --applied | Apache-2.0 on Prisma’s GitHub repository, now prisma/orm; paid features not stated in the Prisma Migrate docs | The app already uses Prisma |
| Drizzle Kit | Change-based with generate and migrate; push also writes the schema straight to the database | Yes: pull --init introspects an existing database and marks it as an applied migration | Apache-2.0 on the drizzle-orm repository | The app already uses Drizzle |
| Atlas | Declarative and versioned workflows | Yes: migrate apply --baseline | Community Edition “under an Apache 2 license”; default release binaries under the Atlas MSA | You want drift detection or a declared schema |
| Flyway | Change-based; versioned files recorded in the schema history table | Yes: flyway baseline | The GitHub repository calls itself “the Flyway open source project” (Apache-2.0); Redgate lists Community (“for individuals and education”) and Enterprise editions | Plain SQL files with no ORM |
| Liquibase | Change-based; changesets recorded in DATABASECHANGELOG | Yes: generate-changelog, then changelog-sync | Liquibase Community under the Functional Source License; Liquibase Secure “is commercially licensed” | Several database engines under one team |
| Rails (Active Record) | Change-based; timestamped files recorded in schema_migrations | Not stated in the Rails guide | MIT on the rails/rails repository | The app is a Rails app |
My working rule for choosing: use the tool your framework or ORM already ships, add Atlas or a schema diff where drift detection matters, and never point a second migration tool at the same database. Which tools can reverse a change is a separate question, answered by the article linked under the rollback section below. The phrase “cloud database migration tools” mostly means transfer services for moving a database between hosts, and AWS, Azure and Google each list one; that is the other meaning covered above.
Data migration in Rails, and everywhere else: keep row changes apart from schema changes
A data migration in Rails changes rows, not structure: backfilling a column, splitting a name, fixing bad values. The Rails guide generally advises against doing it in migration files: schema and data changes have different lifecycles, data migrations can be hard to roll back, and they can run long and may lock tables. The guide suggests the maintenance_tasks gem instead.
The Rails guide to Active Record migrations gives its three reasons as separation of concerns, rollback complexity and performance. Rails teams that still want data migrations run like schema ones can use the data-migrate gem, MIT-licensed, which stores them in db/data and records them in a data_migrations table that mirrors the schema one; its README lists support for Rails 6.1 through 8.0.
The same rule holds in any stack, as my working rule: write a data change so running it twice is harmless, run it in batches on a large table, and never put it in the file that adds the column it fills. Where a backfill sits between the expand and contract steps of a deploy is the subject of the deploy page named in the first section.
Database rollback: the undo line every migration states before it runs
A database rollback plan for a migration answers 3 questions before the change runs: can a reverse file undo it, is fixing forward safer, and which backup is the last resort. A reverse file restores structure, not the data a dropped column or table held, so a destructive change needs a backup first.
- Can a reverse file undo it? Write the reverse migration now, or write down why none can exist.
- Is fixing forward safer? Say whether a new corrective file would be less risky than reversing this one.
- Which backup is the last resort, and when was it last restored?
The reverse file is what most tools mean by a rollback migration. In Sequelize’s migrations guide it is one command, npx sequelize-cli db:migrate:undo, which “will revert the most recent migration”; db:migrate:undo:all goes back to the initial state. My working rule is to write the data migration rollback plan into the migration’s pull request, before it runs, as three short answers and the name of the backup.
The answers themselves live elsewhere: how transaction rollback differs from migration rollback, rollback or fix forward, which tools actually reverse a change, when a backup restore is the only path, what the recovery note holds before a migration runs, and the Supabase commands are all in database migration rollback and what Supabase actually reverses.
How to verify it
Version-controlled migrations are verified by applying the full sequence to an empty database and comparing the result with production. The diff must come back empty for tables, columns, indexes, constraints and policies. Anything that appears only in production is a change someone made outside the files, and it becomes the next migration file.
To apply migrations to a clean database that means anything, the database has to match production’s platform, not only its engine. The six steps, each with its pass result and the evidence to keep:
- 01 Create an empty database on production's Postgres major version, with the roles and extensions production's platform provides: on Supabase, a local Supabase stack or a new project. Pass: it starts empty. Evidence: the version and extension list.
- 02 Apply every migration from the first file with the tool's own apply command. Pass: a clean run with no manual step. Evidence: the apply log.
- 03 Dump the resulting schema and the production schema the same way:
--schema-only, one--schemaper app schema, and the same--restrict-keyvalue on both, then strip the comment lines. Pass: two files. Evidence: both files, dated. - 04 Diff them, with the tool's own diff where it has one or a text diff of the two dumps. Pass: no difference in tables, columns, types, indexes, constraints, policies, functions and grants. Evidence: the empty diff, saved with the date.
- 05 Repeat the diff between staging and production. Pass and evidence: as in step 4.
- 06 Load the seed data and run the smoke tests against the clean database. Pass: every smoke test green. Evidence: the test run.
Step 1 is where a plain container trips people: grants and policies that name a platform role fail to apply on a container that lacks the role (my reading). Step 3 needs the restrict key because without one pg_dump “will generate a random one as needed”, so two plain dumps of the same schema never match line for line. Stripping the -- comment lines before the diff is my working rule. For step 6, the seed data is its own task: how to generate realistic fake data for staging.
For step 4, a tool’s own diff reads better than text. Prisma ORM 7’s migrate diff compares against the migrations folder with --to-migrations or against the configured database with --to-config-datasource. Atlas’s schema diff takes two states, --from and --to, and “calculates the differences between them”. On Supabase, use the linked-project diff from the retrofit article: the CLI’s --linked flag “Diffs local migration files against the linked project”, while the unflagged form compares against the local database and never sees a change made in the production dashboard.
Reading the result is simple. A difference that exists only in production is an untracked change, and it becomes the next migration file. A difference that exists only in the files is a migration that never ran. My working rule is to make the check permanent: a CI job that applies the whole sequence to an empty database on every pull request.
<your tool's apply command> # against the empty database
pg_dump --schema-only --schema=public --restrict-key=compare "$CLEAN_DB_URL" > clean.sql
pg_dump --schema-only --schema=public --restrict-key=compare "$PROD_DB_URL" > prod.sql
grep -v '^--' clean.sql > clean.cmp.sql
grep -v '^--' prod.sql > prod.cmp.sql
diff clean.cmp.sql prod.cmp.sql && echo "schemas match"
In the Production Hardening Sprint, deliverable 4.6 is verified this way: apply the migration sequence to a clean test database and compare the resulting schema.
Where the sprint does this
Deliverable 4.6 of the Production Hardening Sprint moves schema changes into ordered migrations committed to the repository, and its verify line is the one quoted under How to verify it. Deliverable 7.5 integrates database migrations into the deployment process with ordering and compatibility checks, and deliverable 7.5 is verified this way: deploy a representative schema change and verify sequencing and failure handling. Both land in the production readiness report, deliverable 13.1, which accounts for all 123 IDs, keeps failures visible until resolved and explains genuine non-applicable items. Outside the sprint: building new product features or modules, completing unfinished core features or business workflows, and rebuilding core functionality that does not yet perform its intended job; moving to a new database is covered under the other meaning above. Deliverable 4.6 sits under the database checks in the published scope, 7.5 under environments, CI/CD and deployment, and 13.1 under handover.
Common questions about schema and data migrations
What are the four types of data migration?
Lists vary, and none is a standard. AWS’s explainer on data migration names four: storage migration, database migration, application migration and business process migration. A schema migration in an app’s repository is a different thing: a versioned file that changes the database’s structure, which is the subject of this page.
How do I roll back a Prisma migration?
Prisma ORM 7 documents two paths. For a migration that failed in production, you generate a down migration with prisma migrate diff before the up migration, run it with prisma db execute, and record it with prisma migrate resolve --rolled-back, since migrate resolve “can only be used on failed migrations”. For a migration that succeeded, you revert schema.prisma to its earlier state and generate a new migration with migrate dev. The down migration reverts the schema, but other changes to data made as part of the up migration are not reverted. Prisma ORM 8, the version its docs now default to, has no down migrations: a rollback is one more migration, planned with prisma migration plan and applied with prisma db migrate.
Does Rails db prepare run migrations?
Yes, when the database already exists. The task’s own description in the Rails source reads “Run setup if database does not exist, or run migrations if it does”. The Rails guide covers the other two cases: with no database, bin/rails db:prepare runs as db:setup does, and when the database exists without its tables, it loads the schema, runs any pending migrations, dumps the updated schema and loads the seed data.
How to rollback migrate in Laravel?
php artisan migrate:rollback rolls back the last “batch” of migrations, which may include several files, and php artisan migrate:rollback --step=5 rolls back the last five. php artisan migrate:reset rolls back every migration. migrate:fresh is a different command: it “will drop all tables from the database” and then runs the migrations, which is why it never belongs anywhere near production.
What is a savepoint in SQL?
A savepoint is a named mark inside a transaction: rolling back to it undoes every command run after the mark and returns the transaction to its state at that point, without abandoning the whole transaction. In PostgreSQL, savepoints “can only be established when inside a transaction block”, and RELEASE SAVEPOINT removes one while keeping the effects of the commands run after it.
The checks in this guide show you where the app is open. The sprint below closes those gaps, tests the result and writes the evidence down.
Built it with AI. Now it has to hold up for real customers.
The Production Hardening Sprint takes the app you already have and builds the production foundation underneath it. Authentication and access rules, payments that stay consistent, error handling, monitoring, backups, automated tests and a documented handover. Our engineers work inside your existing codebase for ten working days. All 123 deliverables are included, and you get the evidence for each one.
See the Production Hardening Sprint →
$2,500 fixed price · 10 working days · One codebase