We migrated a client app from Firebase to Supabase last quarter. The driver was cost — Firebase Firestore reads at the scale they were operating ($580/month) compared to Supabase Pro flat-rate ($25/month) didn’t require a spreadsheet to make the case. The migration took six weeks of focused work. Here’s the plan we used and the gotchas we hit, so you can do it faster if you’re considering the same move.
Why someone would do this
- Firestore read pricing scales linearly with usage. Once you hit ~10M reads/month, you’re paying more than a Supabase Pro plan covers.
- Firestore’s query language is limited — no real joins, no aggregation, no full-text search without a separate Algolia/Elastic.
- Firebase Auth has fewer customization options than Supabase Auth.
- Postgres opens doors: window functions, recursive queries, GIS, vector search via pgvector, anything you’d expect from a real RDBMS.
Why someone wouldn’t
- Firebase’s mobile SDK is best-in-class. If you’re primarily mobile, Supabase’s mobile SDKs are good but not yet ahead.
- Firebase has tightly-integrated services (App Check, Remote Config, A/B Testing). Replacing them piecemeal costs time.
- If your data model is genuinely document-shaped (deeply nested, schemaless), Postgres’s jsonb columns work but feel awkward.
Step 1: data model translation
Map every Firestore collection to a Postgres table. Map every document field to a column. Nested objects either become jsonb columns or get normalized into separate tables. The mapping is the most time-consuming part — plan a day per substantial collection.
Step 2: data export and import
Firebase has a built-in export to Google Cloud Storage. Export to JSON, then write a Node script that streams the JSON into Supabase via the JS client (using the service-role key). For the client we migrated, 4M documents took 3 hours to import.
import { createReadStream } from "node:fs";
import { parse } from "JSONStream";
import { createClient } from "@supabase/supabase-js";
const admin = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
const stream = createReadStream("./firestore-export/orders.json")
.pipe(parse("*"));
let batch: any[] = [];
for await (const doc of stream) {
batch.push(translateDoc(doc));
if (batch.length >= 500) {
await admin.from("orders").insert(batch);
batch = [];
}
}
if (batch.length) await admin.from("orders").insert(batch);Step 3: auth migration
Firebase Auth users export with their UIDs and email addresses. Supabase Auth can be primed with these via the admin API. Passwords don’t transfer — you’ll need to send everyone a password-reset email or move to magic-link login. We used the migration as an excuse to switch to magic-link.
Step 4: replace Firestore queries with Postgres
Every Firestore `.where().orderBy().limit()` chain becomes a Postgres SELECT. The Supabase JS client mirrors the chained-method style, so the translation is largely mechanical. The places where it isn’t are usually queries that were complicated workarounds for Firestore’s limitations — those simplify dramatically in SQL.
// Firestore
const q = query(
collection(db, "orders"),
where("userId", "==", uid),
orderBy("createdAt", "desc"),
limit(50)
);
// Supabase
const { data } = await supabase
.from("orders")
.select("*")
.eq("user_id", uid)
.order("created_at", { ascending: false })
.limit(50);Step 5: replace Cloud Functions with Edge Functions or server-side code
Firebase Cloud Functions either translate to Supabase Edge Functions (for webhook receivers, scheduled jobs) or to Next.js API routes / server actions (for tenant-coupled logic). Most of the client’s Cloud Functions had grown into tightly-coupled logic that moved better into the Next app.
Step 6: Storage migration
Firebase Storage paths can be re-uploaded into Supabase Storage one bucket at a time. We wrote a script that streamed each file from Firebase to Supabase. Total time on the migration: 6 hours for 12GB across 4 buckets. The URLs change, so update every reference in the app code.
Step 7: dual-write phase
For two weeks before cutover, the app wrote to both Firebase and Supabase. Reads stayed on Firebase. This caught translation bugs where the Supabase data didn’t match Firebase. We resolved 23 mismatch types during dual-write.
Step 8: cutover
Reads switch to Supabase, writes continue to both. Monitor for a week. Then writes cut over too. Firebase becomes read-only as a safety net for another 30 days. Then Firebase project is deleted.
Gotchas we hit
- Firestore’s server timestamps don’t map cleanly to Postgres timestamptz. We had a 4-hour bug where new rows were timestamped in UTC but the app expected the tenant’s local time zone.
- Firestore’s array-contains queries map to Postgres array operators awkwardly. Two queries had to be rewritten as separate JOIN tables.
- Firebase Auth UIDs are random strings; Supabase Auth UIDs are UUIDs. Migrating users meant rewriting every foreign key from string-UID to UUID. We added a temporary `firebase_uid` column to map old to new during the dual-write phase.
- Firebase rules don’t translate to RLS policies one-to-one. We had to rewrite the whole authorization layer; in retrospect it was an improvement.
Cost outcome
The client moved from $580/month on Firebase to $25/month on Supabase Pro. Read volume is identical. Write volume is identical. The savings paid for the migration work in five months and continues to compound. The performance is slightly better on Supabase (P95 query time down 40ms) because Postgres indexing is more aggressive than Firestore’s.
When NOT to migrate
If you’re under 1M Firestore reads/month, the cost difference isn’t worth the migration time. If your data model is genuinely deeply-nested and schemaless, Postgres works but it’s not a natural fit. If you’re heavy on Firebase-specific services (App Check, Remote Config), the replacement effort multiplies.
How this lands at FH
We’ve done one Firebase-to-Supabase migration so far. The decision framework holds: cost-driven, query-flexibility-driven, or auth-customization-driven. If you’re running Firebase and curious whether the migration is worth it for you, book a consultation — we’ll quote a fixed-scope migration sprint if the math works in your favor.