Skip to content

Migrating from Firebase to Supabase: The Real Cost and the Step-by-Step Plan

Firebase pricing scales worse than Supabase past a certain point. Here’s the migration plan that worked for one of our clients.

John Cravey with AIFounder4 min readUpdated Jul 6, 2026

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.

Free estimate · 2 minutes

Read the playbook. Now see it for your business.

The posts are the mechanism. The estimate below sketches the version we'd actually ship for your business, at your scale. About a minute, no opt-in.

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.

Answers

Frequently asked questions

Why would anyone migrate from Firebase to Postgres?

Usually for relational queries, for costs that stop scaling with document reads, and for wanting SQL rather than a query API with structural limits. The pull is typically a specific query that is awkward or impossible in a document store, not a general preference.

Why would anyone not migrate?

Because a working system has value that a better data model does not automatically exceed. If the current setup is fine and the team knows it, migrating buys a quarter of risk for a benefit nobody has quantified. Migrate for a named problem, not for architecture taste.

What is the hardest part of the migration?

The data model translation. Denormalized documents become normalized tables, and the decisions made for read performance in one system are exactly the ones you unwind in the other. Everything downstream, meaning the queries, the rules, and the functions, follows from getting that translation right.

How does auth migrate?

Users move, passwords generally do not, because hashes are not portable between systems. That means either a reset flow for everyone or a period of dual authentication. Discovering this late is what turns a planned cutover into an unplanned support incident.

What replaces cloud functions?

Either edge functions or ordinary server-side code in your application, and the second is often better. Functions that existed only because the client could not talk to the database directly frequently disappear entirely once queries run server-side with row-level policies.

What is the dual-write phase for?

Writing to both systems while reading from the old one, so the new database accumulates real data under real load before anything depends on it. It is the step that makes the cutover boring, and it is the step teams skip when the timeline slips.

How should the cutover happen?

Reads move first, writes second, with the old system kept warm long enough to roll back. A cutover that flips both at once has no recovery path if the new system is wrong in a way testing did not reveal, which is the normal outcome rather than the exception.

What gotchas show up during migration?

Timestamps and timezones, ID formats that were strings in one system and typed in the other, arrays and nested objects that need a schema decision, and security rules whose behaviour does not translate directly to policies. All are ordinary and all cost days when discovered late.

What happens to the cost profile?

It changes shape rather than simply dropping. Document-read pricing becomes compute and storage pricing, so a read-heavy workload usually gets cheaper and a write-heavy one may not. Model your own traffic before treating cost as the justification.

How long does a real migration take?

Longer than the data move, which is the part everyone estimates. The schedule is set by the query rewrites, the auth transition, and the dual-write period. A small application is weeks; anything with real users and background jobs is a quarter.

When should you definitely not migrate?

Mid-launch, under a deadline, or with no rollback plan. Also when the motivation is that the current system feels dated. Migrations are worth doing for capability or cost with a number attached, and painful when the reason cannot be stated in one sentence.

What is the first step?

Write down the queries the application actually needs, not the documents it currently stores. That list is the schema requirement, it exposes immediately which parts are hard, and it tells you whether the migration solves the problem before any data moves.

Question we did not answer? Ask us directly and we will answer it here.

John Cravey, Founder
Written by
John Cravey
Founder

Founder of Frontend Horizon. Writes most of the long-form work on the FH blog.

Newer post
Tool Use With Claude: Building Agents That Don’t Hallucinate Your Production Data
Older post
Reducing Third-Party Script Weight: The Audit Pattern That Saves Half Your JS Budget
Keep reading

More from the blog

Supabase·6 min

Supabase Row Level Security: The Multi-Tenant Pattern We Use Across FH Clients

One Postgres database, many tenants, zero data leakage. Here’s the RLS setup that holds up under real production traffic.

Supabase·5 min

Supabase Edge Functions: When They’re Worth It and When They’re Not

Edge Functions are great for jobs that have to live outside your Next app. Not everything does. Here’s the decision framework.

Supabase·3 min

Reading Supabase Logs: The Five Queries That Catch 80% of Production Issues

The Supabase log explorer is underused. These five queries are the first place we look when something’s wrong.