When Wally's team first attempted to migrate their database from MongoDB to Postgres, they lost 72 hours of development time and nearly broke two weeks of billing. They had underestimated the complexity of moving from a non-relational schema to a strict SQL model and lacked an ORM to handle the heavy lifting. However, once they tried Prisma, the story changed: the entire migration took four days, with no service downtime, and a reliable rollback system.
Photo: fabio on Unsplash
This is not an isolated case. Prisma has evolved from being "just another trendy ORM" to becoming the centerpiece of migration architectures in startups that need to switch their data stack. Let's break down how they achieved this, what technical errors they avoided, and why this tool is outpacing TypeORM and Sequelize in 2026.
The Real Context: Why Wally Needed to Abandon MongoDB
Wally is a Mexican fintech company that automates cash management for small businesses. They started in 2023 with MongoDB because it was quick for prototyping, didn't require rigid schemas, and the whole team came from startups that used it. It worked well until they reached 150,000 active users.
The problem arose when they needed to implement complex financial transactions. Sure, MongoDB has supported ACID transactions since version 4.0, but the reality is that its implementation is slow and requires properly configured replicas. Moreover, it was never the engine's main use case. Every time they ran a nightly bank reconciliation, the cluster would get bogged down. The ad-hoc JOIN queries using $lookup were a performance disaster.
The decision was clear: migrate to Postgres. However, doing so without the right tools meant rewriting the entire data access layer. They would have to manage migrations manually and pray that no edge cases would break production. This is where Prisma came into play.
Why Prisma and Not TypeORM or Writing SQL Directly
Photo: Conny Schneider on Unsplash
Wally's CTO's first proposal was to use TypeORM, the "obvious" choice for TypeScript teams. After two weeks of testing, they ruled it out. TypeORM has a mature ecosystem, but its migration system is fragile. This becomes a problem when making large structural changes. Generating automatic migrations from entity decorators works well for incremental changes, but when migrating from a completely different data model, you end up writing SQL by hand anyway.
The alternative of writing pure SQL with a query builder like Knex.js also fell by the wayside. Wally's team had six developers, three of whom were junior. Managing complex migrations, relationships between tables, and schema validations without a solid ORM would invite production problems every couple of weeks.
Prisma offered something different: a declarative flow based on the schema.prisma file, reliable automatic migrations, and a generated typed client that eliminated 90% of runtime errors. But most importantly, it could serve as a bridge between databases.
The Trick of Dual Introspection
Prisma has a little-documented but powerful command: prisma db pull. This command connects to an existing database and automatically generates a Prisma schema based on the current structure. Wally used this to create an intermediate representation of their MongoDB schema before jumping to Postgres.
The workflow was:
- Connect Prisma to MongoDB using the
mongodbconnector (supported since Prisma 3.12). - Run
prisma db pullto generate a base schema. - Manually refactor that schema to fit an optimal relational model.
- Apply that refined schema to a clean Postgres instance.
- Write data migration scripts using Prisma Client as an abstraction.
This methodology allowed them to have total control over the data transformation without directly touching SQL. Additionally, they had the assurance that the Prisma client validated types at every step.
Step-by-Step Migration Architecture
Now, let’s get technical. Here’s how the actual implementation went down.
Step 1: Dual Simultaneous Connection
Wally configured Prisma to work with two datasources at the same time. This isn’t in the official documentation because Prisma officially supports only one datasource per schema, but there’s a workaround: using multiple schema files.
They created two files:
schema-mongo.prisma:
datasource db {
provider = "mongodb"
url = env("MONGO_URL")
}
model Transaction {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String
amount Float
createdAt DateTime @default(now())
}
schema-postgres.prisma:
datasource db {
provider = "postgresql"
url = env("POSTGRES_URL")
}
model Transaction {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
amount Decimal @db.Decimal(10, 2)
createdAt DateTime @default(now())
}
They generated two distinct Prisma clients using the --schema flag and imported them with different aliases:
import { PrismaClient as MongoClient } from './generated/mongo'
import { PrismaClient as PostgresClient } from './generated/postgres'
Step 2: Batch Migration Script
The team developed a Node.js script that ran during low traffic hours (3 AM - 6 AM CDMX time). The logic was simple but effective:
const mongoClient = new MongoClient()
const pgClient = new PostgresClient()
async function migrateTransactions(batchSize = 1000) {
const totalCount = await mongoClient.transaction.count()
let migrated = 0
while (migrated < totalCount) {
const batch = await mongoClient.transaction.findMany({
skip: migrated,
take: batchSize
})
const transformed = batch.map(tx => ({
userId: parseInt(tx.userId), // Convert string to int
amount: new Decimal(tx.amount),
createdAt: tx.createdAt
}))
await pgClient.transaction.createMany({
data: transformed,
skipDuplicates: true
})
migrated += batch.length
console.log(`Migrated ${migrated}/${totalCount}`)
}
}
What surprised me the most is that the key here is skipDuplicates: true, allowing them to resume the migration if it failed midway. This was vital when a network failure interrupted the migration at batch 340.
Step 3: Dual Write Period
For two weeks, Wally ran in hybrid mode: every new transaction was written simultaneously to both MongoDB and Postgres. They used a "dual write with primary read" pattern:
async function createTransaction(data: TransactionInput) {
// Primary write to Postgres
const pgTx = await pgClient.transaction.create({ data })
// Secondary write to Mongo (non-blocking)
mongoClient.transaction.create({
data: {
id: pgTx.id.toString(),
...data
}
}).catch(err => {
logger.error('Mongo write failed', err)
// Does not block the request
})
return pgTx
}
Reads continued to come from Postgres. MongoDB acted as a backup in case they needed to roll back.
Step 4: Automated Cross-Validation
Every night, a cron job compared records between both databases for inconsistencies:
async function validateMigration() {
const pgCount = await pgClient.transaction.count()
const mongoCount = await mongoClient.transaction.count()
if (Math.abs(pgCount - mongoCount) > 100) {
await sendAlert('Desynchronization detected')
}
// Random sample validation
const sample = await pgClient.transaction.findMany({
take: 1000,
orderBy: { id: 'desc' }
})
for (const tx of sample) {
const mongoTx = await mongoClient.transaction.findUnique({
where: { id: tx.id.toString() }
})
if (!mongoTx || mongoTx.amount !== tx.amount.toNumber()) {
await sendAlert(`Mismatch in tx ${tx.id}`)
}
}
}
This system detected 43 transactions with discrepancies, all caused by a bug in decimal rounding during the initial transformation.
What Prisma Solved (and What It Didn’t)
Prisma isn’t magic. There were things that Wally's team had to resolve manually:
What Prisma Did Well:
- Automatic TypeScript type generation eliminated schema mismatch errors.
- The
prisma migratesystem was 100% reliable for incremental changes in Postgres. - The introspection system (
db pull) sped up the design phase of the new schema. - Prisma Studio facilitated manual data inspection during validation.
What Required Extra Work:
- Transforming the ObjectId (24-character string) to sequential integers needed a persistent conversion map.
- The many-to-many relationships that were embedded arrays in Mongo required explicit pivot tables.
- The performance of the Prisma client with complex queries (joins of 4+ tables) was about 15% lower than pure SQL.
- Handling optional/nullable fields between schemas required extensive manual validation.
The Result: Four Days vs. Four Months
Let’s compare with the previous failed attempt without Prisma:
Attempt 1 (without Prisma, just SQL scripts):
- 72 hours of development.
- 14 production bugs.
- Full rollback necessary.
- Estimated loss: ~$18,000 USD in wasted development hours.
Attempt 2 (with Prisma):
- 4 days of active migration.
- 2 weeks of dual writing.
- 3 minor bugs detected before turning off Mongo.
- Total cost: ~$6,000 USD in development hours.
Most importantly: zero downtime. Wally's users never noticed the migration.
The Landscape in 2026: Why Other Tools Are Falling Behind
Prisma isn't the only migration tool on the market, but it's gaining ground rapidly. Drizzle ORM appeared in 2024 as a lighter alternative, but its migration system is still manual and error-prone. TypeORM remains popular in legacy projects, but its development has stagnated.
What sets Prisma apart is the complete ecosystem: Prisma Migrate, Prisma Studio, Prisma Accelerate (query caching), and now Prisma Pulse (real-time change data capture). It’s a full data management stack, not just an ORM.
For growth-stage startups needing to switch technologies without breaking everything, Prisma is becoming the default choice. Cases like Wally’s demonstrate why: the difference between a controlled migration and a production disaster can lie in the tools you choose.
Is your startup considering a database change? What’s holding you back?