Founder & Lead Engineer, RAITHub
RAITHub ships and tests production software. See QA as a Service or talk to us.
To move a live SaaS from Firebase to PostgreSQL without downtime, never do a big-bang switch. Model your Firestore documents as relational tables, then dual-write: write to both systems, backfill the history, verify they match, switch reads to Postgres feature by feature, then stop writing to Firebase. The hard part is not moving bytes; the two model data differently, so the schema redesign is the real work.
If you would rather have the migration planned and run for you, see how RAITHub would build this below. This guide covers the Firebase-specific decisions; the general zero-downtime technique it relies on, expand-and-contract and lock-safe statements, is in zero-downtime database migrations for SaaS, and if a migration has already gone wrong, start with recovering from a broken migration.
Should you actually migrate off Firebase?
Only for a concrete reason, because migrating a live data store is real risk. The good reasons are structural: you need relational queries and joins Firestore cannot do well, you want strong multi-row transactions and constraints, query or read costs are climbing with scale, or you need SQL-level reporting and tenant isolation with row-level security.
The bad reason is "Postgres is better". Firebase is excellent for real-time sync, simple access patterns and fast starts; if your product lives on those, migrating buys you cost and risk for little gain. Decide by what your queries and your bill actually need, not by preference.
| Keep Firebase if | Move to Postgres if |
|---|---|
| Your access patterns are simple key lookups and real-time sync | You need joins, aggregates and reporting across entities |
| Read and query costs are modest and predictable | Query or read costs climb faster than your users |
| You rarely need multi-document transactions | You need strong multi-row transactions and constraints |
| Your tenancy model is simple | You want row-level security for strict tenant isolation |
Why can't you just copy the data across?
Because Firestore stores denormalised documents in collections, and Postgres stores normalised rows in related tables. A collection does not map to one table one-to-one, and getting that mapping wrong is where most of these migrations fail.
The decisions that bite: a nested object or array inside a document becomes either columns, a child table, or a JSONB column, and the right choice depends on how you query it. Data you duplicated across documents for Firestore's sake should usually be normalised into one row and referenced. Firestore's string document IDs become your primary keys, so keep them to make the mapping traceable. And Firestore has no schema, so real data often violates the shape you think you have; you will find nulls, missing fields and type drift during the backfill, not before. Model the target schema first, on paper, against your real query patterns, before you write any migration code.
What does the dual-write migration look like, phase by phase?
| Phase | What happens | Source of truth | Safe to roll back? |
|---|---|---|---|
| 1. Model & create schema | Design Postgres tables from query patterns; create them empty | Firebase | Yes: Postgres is unused |
| 2. Dual-write | Every write goes to Firebase and Postgres; reads still from Firebase | Firebase | Yes: stop writing to Postgres |
| 3. Backfill | Copy historical documents into Postgres in batches | Firebase | Yes |
| 4. Verify | Compare row counts and sampled records between the two | Firebase | Yes |
| 5. Cut over reads | Switch reads to Postgres one feature at a time, still dual-writing | Shifting to Postgres | Yes: flip reads back to Firebase |
| 6. Contract | Stop writing to Firebase; retire it once confident | Postgres | Harder: keep a Firebase export first |
At every phase both systems are consistent enough that you can step back. Reads move feature by feature, so a problem shows up on one screen, not the whole product. This is expand-and-contract applied across two databases instead of one schema.
How do you dual-write safely?
Write to your existing system (Firebase) first, then mirror the write to Postgres, and make the Postgres write not break the request if it fails during the migration window. The goal is to populate Postgres going forward while Firebase stays authoritative, so a bug in the new path never costs a customer their data.
// During migration: Firebase stays the source of truth.
// The Postgres mirror must never break a live request.
async function saveInvoice(invoice: Invoice): Promise<void> {
await firestore.collection('invoices').doc(invoice.id).set(invoice)
try {
await pg.query(
`INSERT INTO invoices (id, tenant_id, amount_cents, status, created_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO UPDATE SET
amount_cents = EXCLUDED.amount_cents,
status = EXCLUDED.status`,
[invoice.id, invoice.tenantId, invoice.amountCents, invoice.status, invoice.createdAt],
)
} catch (err) {
// Log and alert; do NOT fail the request on the mirror during migration.
logMirrorFailure('invoices', invoice.id, err)
}
}
Keep the original Firestore document ID as the Postgres primary key, and store money as integer minor units (cents), not floats, so no value drifts in translation. The ON CONFLICT upsert makes the mirror idempotent, so a retried write cannot create a duplicate. Once reads have cut over and Postgres is the source of truth, the error-swallowing comes out and a failed Postgres write becomes a real error again.
How do you backfill and verify?
Backfill the history in small batches from a script, not in one pass, so you never hold a long transaction and can stop and resume. Then verify before you trust it, because an unverified migration is a guess.
-- Verify after backfill: counts should match per tenant,
-- and spot-check that sampled records are identical.
SELECT tenant_id, count(*) AS pg_rows
FROM invoices
GROUP BY tenant_id
ORDER BY tenant_id;
-- Compare against the same per-collection counts from Firestore.
Verification is three checks: total and per-tenant row counts match, a random sample of records is identical field by field, and the mismatches the backfill logged (missing fields, type drift) are each explained. Expect the backfill to surface bad data Firestore's schemalessness hid; fixing it is part of the migration, not a surprise. Watch database load while the backfill runs and slow it down if replication lag or load climbs. The batching and safe-statement rules are in zero-downtime migrations for SaaS.
How do you cut over and retire Firebase?
Switch reads to Postgres one feature at a time, behind a flag, while still dual-writing. If a screen misbehaves, flip that one read back to Firebase; the data is still there and still current. Move the whole product over feature by feature, watch for discrepancies, and keep dual-writing until you are confident.
Only then contract: stop writing to Firebase, make Postgres the sole source of truth, and remove the error-swallowing from the write path. Export Firebase one last time and keep the export before you decommission anything, because the contract step is the one that is hard to undo. For a multi-tenant product this is also the moment tenant isolation should be enforced in the database with row-level security, covered in the PostgreSQL row-level security guide.
How long does a Firebase-to-Postgres migration take?
The schema modelling is the slow part: days to a couple of weeks depending on how many collections and how denormalised they are. Dual-write, backfill, verify and a feature-by-feature cutover then run over weeks of calendar time but little downtime, because the work is background. The main risk of doing it yourself is the schema mapping: forcing documents into the wrong table shapes, or discovering during backfill that the data does not match the schema you assumed, and having to redesign mid-migration.
Buy, build or hire?
| Option | What you get | Choose this when |
|---|---|---|
| A managed Firestore-to-SQL export/sync | A tool that streams Firestore changes into a SQL store | You want a mirror for reporting, not a full application cutover |
| Keep Firebase, add Postgres alongside | Firebase for real-time, Postgres for relational queries | Only part of the product needs relational power; a full migration is not worth the risk |
| Migrate in-house | Your team models the schema and runs the dual-write plan | Someone knows both Firestore and relational modelling and has the time |
| Hire a fixed-scope migration | A modelled schema, dual-write, verified backfill and a staged cutover, with tests | The product is live with paying customers and the data cannot be put at risk |
How RAITHub would build this
Because moving money and records between databases without losing or corrupting them, under retries and load, is exactly the kind of data-correctness work RAITHub builds and tests.
- Scope: model your Firestore collections into a relational schema from real query patterns; build the dual-write layer with idempotent upserts; write a batched, resumable backfill and a verification suite; cut reads over feature by feature behind flags; add row-level security for tenant isolation; and contract off Firebase once verified, keeping a final export.
- Timeline: a focused migration fits a backend engagement (6–12 weeks depending on how many collections and how denormalised), or a fixed-scope phase inside a larger SaaS build, confirmed in the written quote.
- You receive: the schema and migration code in your repository, verification and regression tests gated in CI, a runbook with the phase plan and rollback steps, and full IP under NDA. Production data stays in accounts you own.
- Proof: Sundor Skin has 146 PostgreSQL tables, 76 migrations replayed in CI and 530+ tests; TheSkinProof, the founder's own venture, moves money and stock inside row-locked transactions across 217 endpoints (750+ tests); PropDesk has 1,024 tests. See the SaaS development service.
When you don't need us
- Firebase still fits your access patterns and bill. Do not migrate on preference; the risk outweighs the gain.
- Only reporting needs SQL. Mirror Firestore into a SQL store for analytics and keep the app on Firebase.
- You want engineers placed in your team. RAITHub offers fixed-scope work and dedicated teams, not staff augmentation.
If your SaaS has outgrown Firebase and you need it moved to Postgres without losing data or taking customers offline, send RAITHub your collections and your query patterns, and book the free 15-minute technical audit.
Documentation checked on 11 October 2026.
Frequently asked questions
Can I migrate from Firebase to Postgres without downtime?
Yes, with a dual-write migration: write to both systems, backfill the history in batches, verify the two match, switch reads to Postgres one feature at a time while still dual-writing, then stop writing to Firebase last. Avoid a big-bang switch, which risks data loss and an outage.
Why is migrating off Firestore hard?
Because Firestore stores denormalised documents and Postgres stores normalised relational rows, so collections do not map one-to-one to tables. Nested objects become columns, child tables or JSONB depending on how you query them, and Firestore's schemalessness hides bad data you only find during the backfill. The schema redesign is the real work.
Should I move off Firebase at all?
Only for a concrete reason: you need joins and reporting, strong multi-row transactions and constraints, lower read costs at scale, or row-level tenant isolation. If your product lives on real-time sync and simple lookups, migrating buys cost and risk for little gain.
How do I keep data consistent during the migration?
Keep Firebase as the source of truth until reads cut over. Dual-write with idempotent upserts keyed on the original document ID, store money as integer minor units, and do not let a failed Postgres write break a live request during the migration window. Verify with per-tenant row counts and sampled record comparisons.
What do I do with nested Firestore documents in Postgres?
Decide per field by how you query it: turn an object into columns if you filter on its fields, into a child table if it is a repeating list you join or aggregate, or into a JSONB column if you only read it whole. Normalise data you duplicated across documents for Firestore's sake.
How long does a Firebase-to-Postgres migration take?
Schema modelling takes days to a couple of weeks depending on how many and how denormalised your collections are. The dual-write, backfill, verify and feature-by-feature cutover then run over weeks of calendar time with little downtime, because the work happens in the background while Firebase stays live.
Related posts
Ready to discuss your project?
Book a free 15-minute technical audit with our engineering team.