SassTurf
BlogBuilding
Building

Database Migration: A Founder's Guide (2026)

Database migrations explained for founders — schema migrations (the versioned kind you run every deploy) vs moving providers, the tools, and how not to lose data.

Shubham Soni
Shubham Soni
Jul 14, 2026 · 9 min read
Table of contents6 sections
  1. 01Two Words That Sound Identical and Mean Nothing Alike
  2. 02Schema Migrations: The Versioned Kind You Run on Deploy
  3. 03The Golden Rules I Learned by Breaking Them
  4. 04Moving Providers: The Other Migration
  5. 05Why Plain Postgres Is Your Escape Hatch
  6. 06The 2AM Story (Because There’s Always One)

Two Words That Sound Identical and Mean Nothing Alike

Someone messages me, “hey, how do you handle database migrations?” and I have genuinely no idea what they’re asking. Because “database migration” is two completely different jobs wearing the same three syllables, and half the confusion I see from new founders is them reading a blog about one when they needed the other.

Let me kill the ambiguity right now, because the whole rest of this post hangs on it.

Meaning one: schema migration. You added a phone_number column to your users table. (If “schema” itself is fuzzy, here’s what a database schema is in plain English.) That change has to reach your production database in a controlled, repeatable way. You’ll do this a hundred times over a product’s life — every time you add a table, rename a column, add an index. This is the daily-driver meaning, and it’s what people usually mean.

Meaning two: moving your database somewhere else. Your data physically lives on provider A (say, Supabase or some VPS) and you want it on provider B (say, Neon). You do this rarely — maybe once or twice in a product’s entire life, usually when a bill scares you or a provider annoys you.

Same word. One is a deploy step you’ll run this week. The other is a scary one-time move. I’ll cover both, because you’ll eventually need both — and both are places I’ve watched people torch production data because they got cavalier.


Schema Migrations: The Versioned Kind You Run on Deploy

Here’s the mental model that took me embarrassingly long to internalize. Your database schema is code. Not “like” code — it is code, and it deserves the same version control your app gets.

A schema migration is a small, ordered file that says “take the database from state N to state N+1.” Add a column. Create an index. Backfill a value. Each one gets a timestamp or a sequence number, lives in a migrations/ folder in your repo, and gets committed alongside the app change that needs it. Your migration tool keeps a little bookkeeping table in the database itself — Prisma calls it _prisma_migrations — that records which files have already run. On deploy, the tool looks at that table, sees files 1 through 12 already ran, and applies 13, 14, 15. That’s the entire trick.

Why not just log into the database and run ALTER TABLE by hand? Because you have a local database, a staging one, a production one, maybe a teammate’s local one too. Hand-editing means they drift apart, and drift is how you get “works on my machine” bugs that live in your schema. Migrations make it reproducible: a fresh database plus the full migration history equals the exact structure production has. No guessing.

The tools, by ecosystem

You don’t pick a migration tool in a vacuum — you mostly inherit it from your stack. Here’s the honest lay of the land in 2026:

ToolEcosystemHow it worksReach for it when
Prisma MigrateNode / TypeScriptEdit the schema file, it generates + applies SQLYou want a batteries-included ORM
Drizzle KitNode / TypeScriptgenerate the SQL, then migrate to applyYou want SQL-first, lightweight, typed
AlembicPythonAutogenerates revisions from SQLAlchemy modelsYou’re on Django-adjacent Python / FastAPI
FlywayJVM / anythingPlain versioned .sql files, run in orderYou want dead-simple SQL and CI/CD
LiquibaseJVM / anythingChangelogs in XML/YAML/JSON/SQLYou need database-agnostic enterprise features

If you’re building the kind of Next.js SaaS I usually build, it’s a straight fight between Prisma and Drizzle. Prisma gives you the whole ORM and a slick prisma migrate dev that watches your schema file and writes the SQL for you. Drizzle is closer to the metal — you run drizzle-kit generate to produce a SQL file you can actually read and review, then drizzle-kit migrate to apply it. I lean Drizzle these days precisely because I can see the SQL before it runs, and seeing the SQL is what saves you at 2AM.

One Prisma detail worth knowing so it doesn’t spook you: prisma migrate dev spins up a temporary “shadow database” to detect drift and check your migration is safe. That’s a development-only thing. In production you run prisma migrate deploy, which just applies pending migrations with no shadow database and no resetting — the command your CI/CD pipeline runs, never you, manually, at midnight.

On Python it’s Alembic, autogenerating revisions from your SQLAlchemy models. And if you want raw SQL files with zero ORM in the middle, Flyway (now a Redgate product) and Liquibase are the rock-solid old guard — usually overkill for a solo founder, but worth knowing they exist.


The Golden Rules I Learned by Breaking Them

The tool matters less than the discipline. Here are the three rules I will die on, each one earned the hard way.

1. Never, ever edit a migration that already shipped. This is the big one. Once a migration file has run on production, it is frozen. It’s history. If you go back and change file 12 because you found a typo, your local database (which never ran the old version) does one thing, and production (which already ran it) does another, and now your migration history is a lie. The bookkeeping table thinks 12 ran, but “12” now means something different everywhere. To fix a mistake, you write a new migration — 16 — that corrects it. Forward only. Always forward.

2. Back up before every migration. Every single one. I don’t care that it’s “just adding a column.” A migration is the one routine operation that can destroy data in a single command — a bad DROP COLUMN, a rename that Prisma decided to implement as drop-and-recreate, a type change that silently truncates. Before I run anything against production, I take a snapshot. On Neon this is almost free because of branching — you can branch the database, which is a near-instant copy-on-write clone, run the migration against the branch, and only promote it once you’re sure. That safety net alone is worth choosing a modern Postgres host.

3. Expand, then contract — never rename in one shot. This is the rule that separates “we had 10 seconds of downtime” from “we had zero.” Say you want to rename full_name to name. The naive move is one migration that renames the column. But for the moment between “migration ran” and “new code deployed,” your old code is querying a column that no longer exists. Boom, errors. The safe pattern is three steps across separate deploys:

  • Expand: add the new name column, backfill it from full_name, and have your code write to both.
  • Migrate reads: ship code that reads from name. Both columns still exist; nothing breaks.
  • Contract: once you’re confident nothing reads full_name anymore, a later migration drops it.

It feels like a lot of ceremony for a rename, and for a hobby project with zero users you can absolutely just rename it and move on. But the day you have paying customers, expand-contract is the difference between a boring deploy and a support-ticket avalanche. (Deployment plumbing like this is exactly the stuff I argue should be the 1% of your work that hums in the background.)


Moving Providers: The Other Migration

Now the scary one-time kind. You’ve been running Postgres somewhere and you want it somewhere else — a bill got ugly, or you got locked out of a feature, or you just want to consolidate onto Neon like I did.

For Postgres, the entire operation is two commands you’ve probably already got installed. pg_dump exports your whole database — schema and data — into a single file. pg_restore (or plain psql for a plain-text dump) loads it into the new one:

pg_dump "postgresql://user:pass@old-host/dbname" -Fc -f backup.dump
pg_restore -d "postgresql://user:pass@new-host/dbname" backup.dump

That’s genuinely most of it for a small database. The -Fc gives you Postgres’s custom compressed format, which restores faster and lets you be selective if you need to. Point the dump at the old host, restore it into the new one, update your app’s connection string, redeploy, done.

The part nobody tells you: the gap between “dump finished” and “app points at the new database” is a window where writes to the old database get lost. For a side project, take the app down for five minutes at 3AM your users’ time and nobody notices. For a real business you need a proper cutover — put the app in read-only mode, do a final incremental sync, then flip the connection string. If your database is genuinely large or can’t tolerate downtime, that’s when you reach for logical replication instead of a flat dump, but I’ll be honest: if you’re at that scale, you have engineers for this, and you’re not reading a founder’s guide.


Why Plain Postgres Is Your Escape Hatch

Here’s the quiet reason I keep telling broke founders to run PostgreSQL on Neon and nothing more exotic: because the provider migration above is only that easy when your database is boring, standard Postgres.

Neon is plain Postgres. Not “Postgres-compatible,” not “Postgres-flavored.” Real Postgres with the wire protocol and the tools you already know. Which means pg_dump from Neon and pg_restore into any other Postgres host on earth just works. There is no lock-in, because there’s nothing proprietary holding your data hostage. If Neon ever raised prices or annoyed me, I could dump my databases and be on Supabase or a raw VPS by dinner. That freedom to leave is exactly why I trust them to stay.

The moment you drift onto something proprietary — a database with a custom API, a NoSQL store with no clean export, a managed service with weird extensions — that easy pg_dump escape hatch closes, and moving becomes a migration project instead of a migration command. (I went down the whole rabbit hole of when you actually need something beyond plain Postgres — ClickHouse, Timescale, per-user SQLite — in the advanced database guide; the short version is: almost never.) Choosing boring, portable tech is a decision your future, panicking self will thank you for.


The 2AM Story (Because There’s Always One)

Back when I was building Clickly, my URL shortener, I ran a migration against production at some unholy hour to change a column type. I’d tested it locally, it was fine, ship it. What I didn’t clock was that the “type change” my tool generated was secretly a drop-and-recreate, and the recreate didn’t preserve the old values the way I assumed. A chunk of link-analytics data just… evaporated. Clickly had approximately zero real users, so the only person I hurt was me. But I sat there at 2AM staring at a table full of NULLs that used to have data, and I felt the exact cold dread a real business would feel with real customers.

I got the data back. Because — and this is the whole moral — I’d taken a dump right before running it, out of the paranoia I’ve had since day one. Five minutes of pg_restore and it was like nothing happened. The habit I’d built for no good reason on a product nobody used is the same habit that would’ve saved a company.

That’s the thing about migrations. Ninety-nine times out of a hundred they’re a boring, ten-second deploy step you barely think about. The hundredth time is the one that eats your production data, and you never know in advance which one it’ll be. So you back up every time, you never edit shipped history, and you expand before you contract. Not because the docs said so — because the alternative is a 2AM you don’t want to meet.

This is the Broken Engineer Guide. I over-engineer everything, fail at business, and take a backup before every single migration — so that on the one night it matters, I still have my data. Go build something, and back it up first.

Shubham Soni
Written by
Shubham Soni

A decade building, launching, and occasionally breaking SaaS products. I write SassTurf to share what actually moved the needle — free, no fluff.

Keep reading

Building

LangChain Alternatives That Actually Earn Their Place (2026)

9 min read
Building

Kubernetes Deployment: A Founder's Guide to the One File You'd Actually Write (2026)

9 min read
Email Marketing

Out of Office Email Templates That Don't Sound Like a Robot (2026)

8 min read

Enjoyed this? Get the next one.

One useful SaaS essay in your inbox each week. No fluff, unsubscribe anytime.