symflip
All posts

The one thing symflip can't undo: database migrations

symflip rolls back a bad release in one atomic step. But a symlink flip reverts your code, never your database schema. Here's the honest caveat, and the migration pattern that makes it a non-issue.

symflip's whole promise is that a bad release is never a crisis. A deploy lands in its own directory, goes live with a single atomic symlink flip, and if it misbehaves you roll back in one click. The previous release is still on disk, warm and ready, and rollback just points the symlink back at it.

There is exactly one thing that promise does not cover, and I would rather tell you about it plainly than let you discover it during an incident:

Rollback reverts your code. It does not revert your database.

If a release ran a migration that dropped a column, and you then roll back, your old code returns, but the column is still gone. The symlink flip cannot un-migrate a database. No deploy tool honestly can: a schema change is a one-way door in a way a code swap simply isn't.

Why a symlink flip can't help you here

It's worth understanding why, because the why points straight at the solution.

A symflip release is a self-contained directory. Going live is one rename(2) on the current symlink; rolling back is the same rename pointed at the previous release directory. Fast and atomic precisely because nothing but a pointer moves.

releases/
  20260803T101500Z/    previous release (your old code)
  20260803T140200Z/    new release (your new code)
current -> releases/20260803T140200Z    (flip back to undo, instantly)

But your database isn't in either directory. It's shared state that lives outside every release and persists across all of them, which is exactly what you want for your data, and exactly why rollback can't touch it. Flipping current back swaps the code that talks to the database; the database itself stays right where the migration left it.

How symflip runs migrations (and why the order matters)

symflip doesn't ship its own migration engine for your app. Migrations are whatever command you configure: php artisan migrate --force, npm run migrate, rails db:migrate, or your own script. If you leave that field empty, the step is skipped entirely.

When it is set, symflip runs your migration before the atomic swap, while the old release is still serving every request:

build -> migrate -> (pre-activate hook) -> ATOMIC SWAP -> reload -> health check
         |________ still on old code up to here ________|

That ordering is deliberate, and it has a consequence worth sitting with: for a brief window, your old code is running against your new schema. That is perfectly safe for additive changes, like a new nullable column, a new table, or a new index. Old code simply doesn't know the new column exists and carries on. It is not safe if the migration removes or renames something the old code still depends on, because you'd break the live site before the new code even goes up.

So the migration order already nudges you toward additive changes. The rollback caveat is the same lesson from the other end: a migration that isn't safe to run under old code is also, usually, not safe to roll back under old code.

The pattern that makes this a non-issue: expand and contract

The fix is a discipline, not a feature, and it's the same one the teams running the smoothest zero-downtime deploys have used for years. It goes by expand/contract or parallel change, and the single rule at its heart is:

Never ship a schema change and code that depends on the old schema being gone in the same release.

Split every breaking change into additive steps, each of which is independently safe to roll back. Say you want to rename name to full_name:

  1. Expand. Add the new full_name column (additive, so old code ignores it). Deploy code that writes to both columns and still reads the old one. Rolling back is safe: the new column just sits there unused.
  2. Backfill. Copy name into full_name for existing rows, as its own step. No code depends on the result yet, so there's nothing to break.
  3. Migrate reads. Deploy code that reads and writes full_name. Rolling back to step 1's code is still safe, because it was already keeping both columns in sync.
  4. Contract. Only once you're confident you won't roll back past this point, drop the old name column.

At no step in that sequence does a rollback leave you with code and schema that disagree. The one genuinely irreversible action, dropping the column, happens last, deliberately, after the new code has proven itself. You've turned a one-way door into four small doors you can walk back through.

The everyday version of the rule is shorter: make each deploy's migration additive and backward-compatible, and save your destructive changes for a separate, later deploy you're sure about.

What symflip does to keep you on the safe side

The discipline is yours, but symflip is built to support it rather than fight it:

  • Migrations run before go-live. If a migration fails, the swap never happens and your live site is untouched. You're looking at an error, not an outage.
  • It surfaces when a deploy included a migration, and the rollback confirm dialog says in plain language that rolling back does not reverse database migrations. Only you know whether that's safe, so it makes you decide with eyes open rather than clicking through a false promise.
  • Auto-rollback reverts code, and only code. A release that fails its health check can flip back automatically, but it stays honest to the same rule: it restores your old code, never the schema the migration already changed. Which is the whole reason to keep migrations backward-compatible.
  • The previous release stays warm. symflip keeps prior releases on disk (at least the last two) so a code rollback is always instantly available. The fast, safe half of recovery is never in question.

The honest bottom line

A tool that touches your production database should tell you where its guarantees end. symflip's end here: it will roll back your code in a single atomic step, and it will never pretend that also rewinds your database.

The good news is that this stops being scary the moment you design for it. Keep each deploy's migration additive, split breaking changes into expand-then-contract steps, and every release becomes independently reversible, with code and schema in agreement no matter which way the symlink points. The one thing symflip can't undo becomes the one thing you've arranged never to need undone.

Try it free. Connect your first server in minutes.