After my Firebase bill hit $300/month, I knew I had to migrate. But I had a problem: 50,000 downloads, ~10,000 active users, and zero tolerance for downtime or forcing password resets.
Here's how I migrated from Firebase to a custom NestJS + MongoDB stack without breaking everything, and the auth bug that almost ruined it all.
The Challenge
I couldn't just flip a switch. I needed:
- Zero downtime during migration
- No forced password resets (users hate this, I know I do)
- Data consistency between old and new systems
- A way to roll back if things went wrong
The solution? Run both systems in parallel and migrate gradually.
Phase 1: Building the New Backend (2 Days Planning)
Before touching production, I had to build the entire new stack:
The new setup:
- NestJS backend (full API control)
- MongoDB Atlas (free tier)
- $5/month VPS
The first problem: Firestore vs MongoDB data models
Firestore loves subcollections. MongoDB doesn't work that way.
In Firestore, I had:
users/{userId}/progress/{examId}/questions/{questionId}
I had to flatten this for MongoDB:
{
userId: "abc123",
examId: "exam456",
questionProgress: [
{ questionId: "q1", completed: true },
{ questionId: "q2", completed: false }
]
}
Deeply nested subcollections had to become embedded documents or separate collections with references. This took careful planning to avoid slow queries later.
Phase 2: The Dual-Write Strategy
Instead of a "big bang" migration, I used a safer approach: write to both systems simultaneously.
Here's how the dual-write flow worked:
The implementation:
- Installed Firebase Admin SDK in my NestJS backend - This let my new backend talk to Firebase
- Updated my React Native app to call the new NestJS API instead of Firebase SDKs directly
-
Set up dual-write proxying:
- User makes a request to NestJS
- NestJS writes to MongoDB Atlas (new system)
- NestJS immediately mirrors that write to Firestore (old system)
- Both systems stay in sync
For reads: I kept reading from Firestore (source of truth) while verifying MongoDB was populating correctly
The deployment problem:
I needed to push an app update with the new API endpoints, but Play Store review was taking forever. Solution? OTA (Over-The-Air) update. I self-host this, so I pushed the update directly to users without waiting for Google.
Phase 3: The Authentication Migration (The Tricky Part)
This was the hardest part. I had thousands of users with Firebase Auth accounts, and I couldn't force them all to reset passwords.
The lazy migration approach:
Instead of migrating all users at once, I migrated them as they logged in:
Here's the process:
- User tries to log in
- NestJS checks: "Do they exist in MongoDB?"
- If NO:
- Use Firebase Admin SDK to verify their credentials against Firebase
- If successful, capture their plaintext password during that single login
- Hash it with bcrypt and save to MongoDB
- User is now migrated
- If YES: User already migrated, authenticate normally with MongoDB
For inactive users:
I used Firebase's bulk export:
firebase auth:export users.json --project my-project
Then imported their password hashes using Firebase's custom scrypt parameters:
- Signer key
- Salt separator
- Rounds
- Memory cost
This let me pre-migrate users who hadn't logged in yet without forcing resets.
What Broke: The Base64 Signer Key Bug
Everything was ready. I deployed. And then every legacy login failed.
Users who hadn't migrated yet couldn't log in. My error logs were filled with authentication failures.
The problem: Firebase's scrypt signerKey parameter needs to be Base64 decoded before passing it to the hash verification function.
I was treating it as a raw string. My backend wasn't decoding it correctly, so Firebase's scrypt hashes never matched.
The fix:
// Wrong - treating as raw string
const signerKey = process.env.FIREBASE_SIGNER_KEY;
// Right - Base64 decode to buffer
const signerKey = Buffer.from(
process.env.FIREBASE_SIGNER_KEY,
'base64'
);
One line. That's all it took to break authentication for thousands of users.
Luckily, my app is still usable without logging in (users can browse questions), so the damage was limited. But it was a stressful few hours.
What I Kept on Firebase
I didn't migrate everything. Some Firebase services are still cheap/free and not worth rebuilding:
Still using Firebase for:
- Analytics (still free)
- Cloud Messaging/Push notifications (still free)
- Storage for profile pictures (I'm nowhere near the limit, and images are compressed before upload)
No point migrating these when they're working and costing nothing.
The Deep Nesting Problem
Another issue: Firestore lets you nest subcollections infinitely. MongoDB doesn't work that way.
I tried flattening deeply nested Firestore structures into single MongoDB documents. Bad idea. Queries became slow because documents got massive.
The fix: Split deeply nested data into separate collections with references, just like a traditional relational database.
Timeline
- Planning: 2 days
- Execution: 1 day
- Stabilization: A few weeks (fixing the auth bug, optimizing queries, monitoring for issues)
What I'd Do Differently
1. Test the scrypt parameters earlier
I should've written integration tests specifically for Firebase scrypt verification before deploying. That Base64 bug would've been caught immediately.
2. Start with a small cohort
Instead of dual-writing for all users at once, I should've tested with 5-10% of traffic first to catch issues before they affected everyone.
3. Document the data model mapping
I spent way too much time figuring out how to flatten Firestore's subcollections. A clear document mapping old structure ā new structure would've saved hours.
4. Set up better monitoring
I only realized auth was broken when users complained. I should've had alerts for auth failure rate spikes.
The Bottom Line
Migrating from Firebase to a custom backend without downtime is possible, but it's not trivial.
What worked:
- Dual-write strategy (safe, reversible)
- Lazy password migration (no forced resets)
- Keeping Firebase services that are still cheap
What almost broke everything:
- Not properly Base64 decoding Firebase scrypt parameters
- Trying to directly map deeply nested Firestore structures to MongoDB
Was it worth it?
Absolutely. I went from $15-20/month (after optimization) to $5/month total. More importantly, I'm not constantly worried about a spike in usage causing a surprise bill.
If you're considering migrating away from Firebase, take your time, test thoroughly, and run both systems in parallel until you're confident the new one works.
Next up: How I handle deployments and CI/CD for a $5/month production app.
Written while debugging to: [AČa ā Eye Adaba]


Top comments (0)