Migrating Production Data from MongoDB to PostgreSQL with a 2-Hour Cutover
Stack
- PostgreSQL
- MongoDB
- Node.js
On this page
Key results
- Users migrated
- ~8,000
- Customer databases consolidated
- ~60 → 1
- Production cutover
- ~2 hours
- MongoDB
- Retired
Problem
Better Coaching is a multi-tenant SaaS platform that football clubs and academies use to manage coaching, players and training content.
The platform had grown to about 8,000 users across roughly 60 customer platforms, and each customer had its own MongoDB database. That worked early on, but the data had become heavily relational. Users were connected to teams, clubs, roles, sessions, permissions, training content and more, and the existing architecture made several things harder:
- expressing complex relationships cleanly;
- enforcing referential integrity;
- keeping related records consistent;
- running joins and cross-domain queries;
- building reporting;
- debugging data problems;
- evolving the schema safely.
MongoDB wasn’t the only issue. The bigger problem was the combination of the database and a data model that had grown feature by feature. Better Coaching needed a foundation with stronger relational modeling, data integrity, transactions, easier reporting and a cleaner multi-tenant model.
It also had to get there with years of live production data intact and as little disruption to users as possible.
My role
I was the team lead on this migration. I set the direction for the new data architecture and led the team through redesigning the schema, building and rehearsing the migration, and running the production cutover.
A large part of the role was managing risk: deciding when the rehearsals were good enough, and planning the cutover so the final production window held no surprises.
Technical approach
Consolidating per-customer databases
About 60 separate MongoDB databases became one shared, multi-tenant PostgreSQL database. Customer ownership is modeled explicitly with a tenant id column and tenant relationships. Customer data stays logically separate, and the application evolves a single consistent schema instead of the same model across dozens of databases.
Migrating users
Users were the hardest part. A single user could be connected to the product through:
- tenant membership;
- teams;
- roles;
- permissions;
- sessions;
- coaching structures;
- other domain-specific relationships.
Every one of those relationships had to be rebuilt correctly in the new schema. A missing or wrong link could cause subtle problems that wouldn’t show up straight after the migration, which is a big reason the migration was rehearsed so many times.
Custom migration scripts
We wrote custom scripts to transform the MongoDB data into the new structure. They did far more than copy records:
- read the existing production structures;
- mapped old records to the new schema;
- transformed fields where needed;
- reconstructed relationships;
- preserved tenant ownership;
- created new relational references;
- handled inconsistencies and edge cases.
Because the target schema differed so much from the source, this migration logic became a core part of the project.
Rehearsing against production-like copies
Production was never the first place the migration ran end to end. We ran the scripts repeatedly against copies of production data, and each rehearsal surfaced:
- incorrect mappings;
- missing relationships;
- unexpected legacy data;
- performance bottlenecks;
- data that didn’t fit the new schema’s assumptions.
We adjusted the scripts and schema and ran it again, until we were confident the real migration would fit a controlled maintenance window.
Planning the cutover
Unlike an incremental API migration, a database has a strict source-of-truth problem. Keeping MongoDB and PostgreSQL both writable for a long period would have needed complex synchronization and added risk, so we chose a single controlled cutover.
We scheduled it for July, historically Better Coaching’s lowest-traffic period, and put the application in maintenance mode so nothing could change while it ran. The cutover followed a fixed sequence:
- stop application writes;
- run the final migration;
- validate record counts and relationships;
- run automated checks;
- test critical application flows;
- verify manually;
- switch the application to PostgreSQL;
- bring the platform back online.
The two-hour cutover
The final production migration finished in a maintenance window of about two hours. By then most of the risk had already been removed. The cutover was the last run of a process that had been tested many times over.
Decisions & trade-offs
Choosing a relational model
We chose PostgreSQL because of the shape of Better Coaching’s data. Most of the product is relationships, and PostgreSQL let us model them explicitly with:
- foreign-key constraints;
- joins across related entities;
- ACID transactions;
- a clear schema structure;
- predictable queries for reporting and features.
The goal wasn’t to swap MongoDB for SQL. It was a data model that matches how the product actually works.
Redesigning the schema instead of translating it
We expected significant schema work, but once we mapped the existing data it was clear the redesign would be bigger. The MongoDB structure had grown around individual features, and moving it across as-is would have rebuilt the same problems in a different database.
So we avoided a one-to-one conversion and revisited entity boundaries, relationships, ownership, duplicated concepts, required vs. optional relationships, and how tenant separation should work. That widened the scope, but it’s what made the new database solve the problems that started the project.
Results
MongoDB retired
After validation and cutover, all production data ran on PostgreSQL and MongoDB was removed from the architecture. The application wasn’t left maintaining two database technologies.
Stronger data integrity
Foreign keys and constraints now enforce relationships in the database instead of relying entirely on application code, and transactions protect operations that change several related records.
Simpler relationships and reporting
Queries across users, teams, tenants and related entities are easier to write, and reporting no longer has to reason about dozens of separate databases. That helps feature development, debugging and data investigation alike.
A cleaner multi-tenant foundation
About 60 databases became one schema with explicit tenant relationships. That removed schema fragmentation and gives the product one consistent base for future work.
A controlled release instead of a gamble
A migration this size puts the product’s most important asset at risk: customer data. Schema redesign, custom tooling, repeated rehearsals on production data, automated validation, application testing and a deliberately chosen low-traffic window turned it into a planned release with about two hours of downtime.