Flyway has been around since 2010. Liquibase since 2006. dbmate is newer but built on the same lineage. Between them, that’s decades of hard-won engineering on one narrow problem: how do you change a database schema safely, repeatedly, without losing anyone’s data.
Here’s the part that surprised me: none of that engineering ever made it over to NoSQL-to-SQL migrations. Not because it’s secret knowledge — it’s all open source, well documented, battle-tested in production at companies most of us have heard of. It just never got ported. Every NoSQL migration tool I’ve looked at (fireway, mad-migration, a handful of others) was built from scratch, by someone solving their own one-time problem, without borrowing the patterns that already exist one layer over.
So before designing anything new, I went and read how the old tools actually work. Here’s what’s worth stealing.
1. Convention over configuration
Flyway resolves execution order from the filename alone: V2__add_orders_table.sql. No config file, no database of what-runs-when. The number in the name is the order.
Applied: every migration step should produce a versioned, predictably-named file, sitting in your own filesystem, reviewable in git — not hidden state living only inside the tool’s memory.
2. Dirty state as a safety feature, not an annoyance
If a Flyway migration fails partway through, it marks the schema as dirty and refuses to run anything else until a human looks at it and confirms it’s safe to continue.
The interesting bit isn’t the tracking — it’s the refusal. Most tools I’ve seen err toward “keep trying” when something goes wrong. Flyway’s answer is the opposite: stop, and make a human decide. That’s a better default when the thing you might silently corrupt is someone’s production data.
3. Dump the schema after every step
dbmate writes a plain, readable db/schema.sql after each migration — the full current state, always diffable in git.
This sounds almost too simple to be a “lesson,” but it changes what trust looks like. Instead of a log you have to parse, you get a file you can just read. Every step should leave something concrete behind, not just a status message that scrolls off your terminal.
4. Stay a small, boring binary
dbmate is deliberately minimal: one binary, plain SQL, one environment variable for the connection string. No ORM, no plugin system, no framework opinions leaking into your schema.
Boring is a feature here. The moment a migration tool needs its own plugin ecosystem to be useful, it’s stopped being a migration tool and started being a platform — a much harder thing to keep maintained for free.
5. Rollback is the user’s job, and the tool should say so
In Liquibase Community, you write your own rollback scripts. The tool doesn’t invent a magic “undo” button.
That’s more honest than it sounds. A generic “undo” is often not actually safe — depending on what happened in between, reversing a migration cleanly can be a genuinely different (and harder) problem than applying it. Pretending otherwise is how people end up trusting a rollback that quietly makes things worse.
6. Separate the “what changes” from the “how it runs”
Liquibase keeps the changelog (SQL, YAML, JSON, XML — your choice) as a distinct artifact from the execution engine. You can read, review, and version the changelog without running anything.
For anything involving inferred structure — which is exactly what NoSQL-to-SQL migration needs — this separation matters even more. The proposed schema should be something a human reads and edits calmly, in a file, before anything touches a real database. Not something that flashes by in a CLI prompt and gets approved on reflex.
Why none of this made it to NoSQL migrations before now
Two honest reasons, not one.
First: relational schema migration has a stable shape — SQL is a standard, so a tool built for it generalizes to any project using Postgres or MySQL. NoSQL-to-SQL doesn’t have that shape. Firestore, MongoDB, and DynamoDB each model data differently enough that a tool built around one doesn’t transfer cleanly to another, which makes the payoff for polishing one tool much smaller.
Second, and more interesting: even DBeaver — a database client with 8M+ users and real funding — treats NoSQL support as a paid feature, not something that ships free. That’s a signal worth sitting with: the market for polished relational tooling is large and stable enough to sustain 20 years of open-source maintenance. The market for NoSQL migration tooling, so far, hasn’t been.
None of that is a reason to skip these lessons. It’s the reason nobody applied them yet.
I’ve been building a Firestore-to-Postgres migrator (Centauri Migrate) using exactly the six patterns above — versioned steps, a dirty-state guard, a readable schema dump after inference, dry-run by default, and a changelog you review before anything runs. Early days, 84 tests, not yet run against a real production dataset — but the architecture isn’t guessing at this part, it’s borrowed from tools that already proved it works.
Built at 3am, still holding the morning coffee ☕
