Skip to content
FirebaseSupabase
Migration Guide

Firebase to Supabase Migration Guide

Moving from Firestore's document model to Supabase's Postgres is the most common backend migration I get asked about. It is worth doing when your queries outgrow NoSQL — and worth skipping when they haven't.

What Changes
  1. Firestore collections/documents become normalized Postgres tables
  2. Security Rules become Row-Level Security policies
  3. Firebase Auth users move to Supabase Auth (GoTrue) with password hash import
  4. onSnapshot listeners become Supabase Realtime channels
  5. Cloud Functions become Edge Functions (Node → Deno)
Migration Path

Migrate in this order: schema design → data export/import → auth users → app reads → app writes → realtime → decommission. Run both backends in parallel behind a flag until reads are verified. The schema step is 80% of the work — a straight collection-to-table copy recreates NoSQL problems inside Postgres.

In detail

Why teams migrate — and why some shouldn't

The trigger is almost always queries. Firestore is excellent at "fetch this document and its children" and painful at everything relational: joins, aggregates, reporting, ad-hoc filters across collections. Teams end up duplicating data into denormalized mirrors just to make one dashboard query possible, and every duplicated field is a consistency bug waiting for a missed write. Postgres makes those queries one JOIN or one GROUP BY. Cost is the second trigger — Firestore bills per document read, so a poorly-shaped listener that re-reads 500 documents per screen shows up directly on the invoice, while Supabase bills by instance size regardless of read volume.

The honest counter-case: if your data really is document-shaped, your queries are key-value lookups, and offline-first mobile sync matters (Firestore's local persistence is still stronger than anything in the Supabase client), migrating buys you a rewrite and no new capability. I work in both stacks — Callidus runs on Firebase, Pizzeria Bestek on Supabase — and the deciding question is never "which is better" but "what shape are your queries in twelve months".

Schema design: the step that decides everything

Do not copy collections to tables one-to-one. Firestore structures encode workarounds — duplicated user names on every order document, arrays of IDs standing in for join tables, subcollections that exist only because top-level queries were expensive. Migrating those verbatim gives you a denormalized Postgres schema with none of Postgres's benefits. Instead, work backwards from your queries: list the screens and reports the app actually renders, design normalized tables that answer them, then write the transform from the Firestore export to that schema.

Practical mechanics: export with gcloud firestore export or a paginated Admin SDK script, transform in a one-off Node script, and load with COPY or batched inserts. Firestore Timestamps become timestamptz, document IDs are worth keeping as a firestore_id text column during the transition so you can trace any row back to its source document, and map arrays into join tables rather than jsonb unless the data is genuinely unstructured. Expect the transform script to go through several iterations — run it against a staging project until row counts and spot-checks match.

Auth, rules, and the parts people forget

Auth is the highest-risk step because it is the one users notice. Supabase Auth can import Firebase's scrypt password hashes — Firebase uses a modified scrypt, and Supabase's migration tooling accepts the hash parameters from your Firebase project settings, so users keep their passwords instead of being forced through a reset. Export users with firebase auth:export, import via the Supabase CLI or Admin API, and keep the Firebase UID in a column: every foreign key in your new schema that pointed at a Firebase UID needs to resolve to the new auth.users row.

Security Rules translate to Row-Level Security, but not mechanically. Rules run per-request against a document; RLS runs per-row inside the query. A rule like allow read: if resource.data.ownerId == request.auth.uid becomes USING (owner_id = auth.uid()) — but rules that walk to other documents with get() become policies with subqueries against other tables, and those need indexes or every query pays for a sequential scan. Also budget for the long tail: scheduled Cloud Functions become pg_cron jobs or scheduled Edge Functions, Cloud Storage files move to Supabase Storage with new URLs, and FCM push has no Supabase equivalent — that one stays on Firebase or moves to a third party.

Cutover without downtime

Run both backends in parallel and cut over by feature, not by big bang. The sequence that works: ship the new Postgres schema and backfill it from a Firestore export, then add a sync path for writes made during the transition — either dual-writes from the app behind a flag, or a Cloud Function that mirrors Firestore writes into Postgres. Move reads first, screen by screen, because reads are reversible: if a screen misbehaves on Supabase, flip the flag back. Move writes only after reads have been stable, then run a final reconciliation script comparing row counts and recent records before turning Firestore listeners off.

Two things bite during parallel running. Realtime behaves differently — Firestore's onSnapshot delivers the initial state plus deltas, while Supabase Realtime delivers only changes, so screens need an explicit initial fetch before subscribing. And ID generation must move to Postgres (gen_random_uuid() or identity columns) at write-cutover, or the two systems mint conflicting keys. Keep the Firebase project alive but read-only for a few weeks after cutover; it is your rollback and your audit trail while confidence builds.

Live ExamplePizzeria Bestek — Case Study
Other migration guidesView all →
Related

Planning this migration?