Mi­grat­ing from Fire­base to Su­pabase: the hard part is the data mod­el

Al­most every­one who mi­grates from Fire­base to Su­pabase un­der­es­ti­mates the same block: not auth, not stor­age, but re­shap­ing de­nor­malised Fire­store col­lec­tions into nor­malised Post­gres ta­bles with RLS. An hon­est ac­count of where the time and mon­ey ac­tu­al­ly burn — and which apps should not mi­grate at all.
10 min readMatthias RadscheitMatthias Radscheit
Happycodingen-US

TL;DR

When you migrate from Firebase to Supabase, auth and storage are routine: Supabase verifies password hashes natively, and files move in two phases. The expensive part is reshaping the data — turning denormalised Firestore collections into normalised Postgres tables with row level security, and rethinking realtime listeners from scratch.

  • Auth and storage are the easy parts: Supabase Auth verifies Firebase scrypt hashes natively, so no user has to reset their password.
  • The real effort sits in the data model. The official guide copies one collection into one table and dumps nested fields into jsonb — genuine relational remodelling stays manual work.
  • Firestore Security Rules have to be rewritten entirely as row level security in SQL. Path-based rules don't translate one to one.
  • Firestore realtime with offline cache has no direct Supabase equivalent. Client-side caching and conflict resolution are yours to build.
  • If the app is deep in FCM, Crashlytics, Remote Config or ML Kit, a full migration often costs more than it returns — then a partial migration or staying put is the more honest call.

Most mi­gra­tion guides sell the wrong part as the prob­lem. Mov­ing auth and stor­age from Fire­base to Su­pabase is rou­tine — pre­dictable, doc­u­ment­ed, done in a sprint. What blows up projects is the data mod­el.

By the time you read an ar­ti­cle like this, the strate­gic call is usu­al­ly al­ready made: away from Google Cloud, to­wards Post­gres and a self-hostable stack. The rea­sons be­hind it — data sov­er­eign­ty, the cost curve, ven­dor lock-in — I've cov­ered at length else­where. This is about the me­chan­ics. What ac­tu­al­ly hap­pens when you mi­grate Fire­base to Su­pabase, in what or­der the pain ar­rives, and where the bud­get burns if you start un­pre­pared.

Mi­grat­ing Fire­base to Su­pabase means fight­ing the data mod­el

Fire­store and Post­gres hold fun­da­men­tal­ly dif­fer­ent world­views. Fire­store is a doc­u­ment-ori­ent­ed NoSQL data­base op­ti­mised for read per­for­mance by du­pli­cat­ing data. You don't mod­el by en­ti­ties and re­la­tion­ships, you mod­el by ac­cess pat­terns: what does this screen show? Ex­act­ly that lands in a doc­u­ment — re­dun­dant, de­nor­malised, of­ten sev­er­al times over. An or­der car­ries a copy of the de­liv­ery ad­dress, the user­name sits in five col­lec­tions be­cause a join would be ex­pen­sive.

Post­gres flips that. A re­la­tion­al data­base lives on nor­mal­i­sa­tion: one truth per fact, linked through for­eign keys. The step from Fire­store to Post­greSQL is there­fore not a data trans­fer but a mod­el­ling task. You aren't trans­lat­ing rows, you're de­sign­ing a schema from scratch. If you're not yet fa­mil­iar with how Su­pabase is built, the fun­da­men­tals are in my overview of what Su­pabase is, tech­ni­cal­ly.

The of­fi­cial mi­gra­tion guide makes that hon­est­ly vis­i­ble. It de­scribes the tool as some­thing that "copies the en­tire con­tents of a sin­gle Fire­store col­lec­tion into a sin­gle Post­gres ta­ble" (Su­pabase doc­u­men­ta­tion on Fire­store data mi­gra­tion). Three Node scripts han­dle the ex­port, the JSON con­ver­sion and the im­port. Nest­ed fields land in a jsonb col­umn by de­fault. It works — but it pro­duces Post­gres ta­bles that still look like Fire­store on the in­side. One col­lec­tion, one ta­ble, the rest as a JSON blob.

That's not a re­la­tion­al mod­el. It's Fire­store in a Post­gres cos­tume. Leave it there and you give away ex­act­ly the strengths you mi­grat­ed for: joins, con­straints, in­tegri­ty, real queries. The clean re­shap­ing — break­ing col­lec­tions apart, mak­ing re­la­tion­ships ex­plic­it, keep­ing jsonb where it earns its place and only there — is man­u­al work and head work. And it's the line item where the hours pile up.

Fire­base Auth mi­gra­tion: less dra­mat­ic than you'd think

Here's the good news that gets lost in most dis­cus­sions: the Fire­base Auth mi­gra­tion is not a break for your users. No­body has to re­set a pass­word if you do it right.

The rea­son lies in how Su­pabase Auth — GoTrue in­ter­nal­ly — is built. GoTrue ver­i­fies Fire­base scrypt hash­es na­tive­ly through a dis­patch­er that reads the $fb­scrypt$ pre­fix to recog­nise which hash­ing method a stored pass­word uses, along­side bcrypt and Ar­gon2. In prac­tice that means you ex­port the users from Fire­base, im­port them hash and all, and sign-in keeps work­ing with the ex­ist­ing pass­word. On the next suc­cess­ful lo­gin Su­pabase can qui­et­ly re-hash to bcrypt, its de­fault for new pass­words.

The pro­ce­dure is me­chan­i­cal. Two scripts from the com­mu­ni­ty tool­ing fire­base-to-su­pabase do the work: one ex­ports the users as JSON, one im­ports them. The catch: you have to copy four SCRYPT pa­ra­me­ters out of the Fire­base con­sole — base64_sign­er_key, base64_salt_sep­a­ra­tor, rounds and mem_cost. With­out those four val­ues GoTrue can't ver­i­fy the hash­es.

This is ex­act­ly where the only rel­e­vant pit­fall of the auth mi­gra­tion sits. There's a doc­u­ment­ed bug where wrong­ly im­port­ed hash­es land in the data­base with an emp­ty en­crypt­ed_pass­word field. The re­sult: every lo­gin fails even though the users are seem­ing­ly present. The fix is the cor­rect $fb­scrypt$ im­port path. Use the of­fi­cial path, check with a test user be­fore­hand, and you nev­er hit the prob­lem. Auth is pre­dictable when you plan it — not be­cause it's triv­ial, but be­cause the path is ful­ly doc­u­ment­ed and the be­hav­iour de­ter­min­is­tic. If you'd rather move off pass­word lo­gin onto your own iden­ti­ty provider, it's worth eval­u­at­ing Key­cloak as an auth lay­er in par­al­lel.

A note on the tool­ing: fire­base-to-su­pabase is a com­mu­ni­ty repo, not Su­pabase core. It cov­ers auth, Fire­store, stor­age and func­tions, but it's com­mu­ni­ty-main­tained, not a man­aged prod­uct. For a pro­duc­tion move that means: read the scripts first, dry-run them in a stag­ing en­vi­ron­ment, don't run them blind against the live data­base.

Fire­base Stor­age mi­gra­tion and the thing about pri­vate buck­ets

The Fire­base Stor­age mi­gra­tion fol­lows a sim­ple two-phase pat­tern: down­load, up­load. Ex­port files from Fire­base Stor­age, write them into Su­pabase buck­ets. With large vol­umes you work with pag­i­na­tion and batch­es, oth­er­wise you run into mem­o­ry or time­out lim­its.

The one point that catch­es teams out: new Su­pabase buck­ets are pri­vate by de­fault. In Fire­base, ac­cess is of­ten gov­erned by a Se­cu­ri­ty Rule, and much of it is ef­fec­tive­ly pub­lic-read­able. Move files into Su­pabase and for­get to set the ac­cess rules ex­plic­it­ly, and sud­den­ly every avatar, thumb­nail and PDF is un­reach­able — or, on the in­verse mis­take, pub­lic where it should­n't be. That's not a tech­ni­cal prob­lem, it's a check­list ques­tion. You de­cide per buck­et, de­lib­er­ate­ly: pri­vate with signed URLs, or pub­lic. That's pre­cise­ly the se­cu­ri­ty gain, if you use it.

Stor­age, like auth, is a solved prob­lem. Ef­fort: man­age­able and lin­ear with the data vol­ume. Risk: low, as long as you know about the de­fault pri­va­cy.

Mi­gra­tion pit­falls: Se­cu­ri­ty Rules and Re­al­time

The real mi­gra­tion pit­falls hide in two places — and both hang off the con­cept, not the trans­fer.

Fire­store Se­cu­ri­ty Rules are path-based. You write rules along doc­u­ment paths, in a ded­i­cat­ed DSL that grants or de­nies ac­cess de­pend­ing on re­quest.auth and doc­u­ment con­tent. In Su­pabase this trans­lates into row lev­el se­cu­ri­ty: SQL poli­cies per ta­ble, split by SE­LECT, IN­SERT, UP­DATE and DELETE. This is not find-and-re­place. The path-based log­ic has to be rethought as a re­la­tion­al con­di­tion — "may user X see this doc­u­ment" be­comes "which rows of this ta­ble does the pol­i­cy re­lease for auth.uid()".

The pay­off is con­sid­er­able once you grasp it. RLS does­n't just fil­ter di­rect queries, it au­to­mat­i­cal­ly fil­ters re­al­time sub­scrip­tions too. A cor­rect­ly writ­ten pol­i­cy there­by pro­tects read ac­cess across every chan­nel — an ar­chi­tec­tur­al ad­van­tage that Fire­store rules don't of­fer in this form. Why row lev­el se­cu­ri­ty is more than a se­cu­ri­ty fea­ture, and how to struc­ture poli­cies clean­ly, I've writ­ten about sep­a­rate­ly. For the mi­gra­tion, the rule is: bud­get time to rewrite and test the en­tire rule log­ic. This is not a side task.

The sec­ond con­cep­tu­al break is re­al­time. Fire­store's on­Snap­shot is more than a live up­date: it brings a lo­cal of­fline cache and con­flict res­o­lu­tion, the app keeps work­ing with­out a net­work and syncs lat­er. Su­pabase Re­al­time streams Post­gres WAL changes over Web­Sock­ets (doc­u­men­ta­tion on Post­gres Changes) — fast and clean, but with­out na­tive of­fline per­sis­tence. If you re­lied on Fire­store's of­fline be­hav­iour, you build the client caching your­self, for ex­am­ple with TanStack Query. For a clas­sic web app that's of­ten a non-is­sue. For an of­fline-first mo­bile app it's a sep­a­rate im­ple­men­ta­tion block that vis­i­bly rais­es the cost of the mi­gra­tion.

How deep is the app in the Fire­base ecosys­tem?

Here I have to name the counter-po­si­tion fair­ly, be­cause in many cas­es it's right. If an app uses only Fire­store, Fire­base Auth and stor­age, the move is a clear­ly bound­ed project. But if it sits deep in the Fire­base mo­bile ecosys­tem, the maths changes fun­da­men­tal­ly.

FCM for push no­ti­fi­ca­tions, Crash­lyt­ics for crash re­port­ing, Re­mote Con­fig for fea­ture flags, ML Kit for on-de­vice mod­els, Fire­base An­a­lyt­ics — none of these has a di­rect Su­pabase equiv­a­lent. Su­pabase has no push ser­vice of its own; push still runs over FCM or APNs, trig­gered from Edge Func­tions. So the mi­gra­tion does­n't re­move the FCM de­pen­den­cy, it just shifts where you call it from. Crash­lyt­ics, Re­mote Con­fig and ML Kit have no coun­ter­part — you re­place them through third par­ties like Sen­try, or you sim­ply keep Fire­base for those parts.

That's the hon­est core of the trade-off: the more an app leans on these mo­bile-spe­cif­ic build­ing blocks, the fur­ther the mi­gra­tion drifts from a pure data­base swap. "Fire­store to Post­greSQL" then be­comes "Fire­store to Post­greSQL plus re-wiring FCM plus re­plac­ing Crash­lyt­ics plus re­build­ing Re­mote Con­fig". You have to price these items in­di­vid­u­al­ly be­fore you name a bud­get. A par­tial mi­gra­tion — data­base to Su­pabase, mo­bile teleme­try stay­ing on Fire­base — is in such cas­es of­ten the eco­nom­i­cal­ly clean so­lu­tion, not a com­pro­mise born of weak­ness.

What a mi­gra­tion re­al­is­ti­cal­ly costs

Num­bers with­out con­text are worth­less, so here's the frame first. A clean ini­tial im­ple­men­ta­tion of a self-host­ed Su­pabase stack — set­up, in­te­gra­tion, se­cu­ri­ty, test­ing — runs, in our ex­pe­ri­ence, to a one-off €5,000 to €20,000 in de­vel­op­ment ef­fort. A mi­gra­tion comes on top and spreads it­self high­ly un­even­ly.

Auth and stor­age are the cheap blocks: doc­u­ment­ed, de­ter­min­is­tic, done in days. The data trans­fer it­self — run­ning the scripts, copy­ing col­lec­tions — is man­age­able too. What dri­ves the bill is the part no­body likes to talk about: re­shap­ing the data mod­el, rewrit­ing the Se­cu­ri­ty Rules as RLS and, where rel­e­vant, re­build­ing re­al­time of­fline be­hav­iour and Fire­base mo­bile fea­tures. At a blend­ed rate of rough­ly €120 per de­vel­op­er hour, the com­plex­i­ty of the data mod­el de­cides whether a mi­gra­tion lands at the low­er or up­per end of the range.

The strate­gic point be­hind it: the run­ning costs of a self-host­ed stack fall over time, while the cost of man­aged ser­vices ris­es with the pro­jec­t's suc­cess. A mi­gra­tion is a one-off in­vest­ment against a cost curve that re­vers­es. How that plays out over three years and what re­al­is­tic op­er­at­ing costs look like, I cov­er in the TCO com­par­i­son for Su­pabase in pro­duc­tion. If you're af­ter the full strate­gic log­ic be­hind the switch — why Su­pabase is a se­ri­ous sov­er­eign­ty op­tion for DACH or­gan­i­sa­tions — you'll find it on our Su­pabase overview page.

Some apps should not mi­grate

Now the un­com­fort­able part, which I'll state open­ly even though hap­py­cod­ing earns mon­ey on mi­gra­tions. Not every Fire­base app be­longs on Su­pabase. Some should stay ex­act­ly where they are.

If an app lives most­ly on Fire­base-spe­cif­ic mo­bile fea­tures — push, Crash­lyt­ics, Re­mote Con­fig, ML Kit, on-de­vice mag­ic — and there's no gen­uine sov­er­eign­ty or cost re­quire­ment be­hind it, then a mi­gra­tion is an ex­pen­sive move with­out a des­ti­na­tion. You swap a work­ing ar­chi­tec­ture for an­oth­er, re­build half the Fire­base ecosys­tem through de­tours, and pay de­vel­op­ment bud­get that pro­duces no mea­sur­able ad­van­tage any­where. You'd be mi­grat­ing for a feel­ing, not for a re­quire­ment.

The hon­est test comes down to two ques­tions. First: is there a con­crete dri­ver — GDPR pres­sure, a data-sov­er­eign­ty man­date from the board, a Fire­store cost curve that ex­plodes with suc­cess? The data-pro­tec­tion di­men­sion I've worked through sep­a­rate­ly, be­cause Google too, as a US cor­po­ra­tion, stays sub­ject to the CLOUD Act de­spite an EU stor­age lo­ca­tion — read it in my piece on data pro­tec­tion as a strate­gic de­ci­sion. Sec­ond: does the app move most­ly on the data lay­er, or does its val­ue hang on mo­bile-spe­cif­ic Fire­base in­fra­struc­ture? If the val­ue sits in the data and there's a dri­ver, the mi­gra­tion al­most al­ways pays off. If the val­ue hangs on Fire­base-only fea­tures and the dri­ver is miss­ing, stay­ing put is the pro­fes­sion­al an­swer.

What I tell de­ci­sion-mak­ers

Treat the mi­gra­tion not as a data trans­fer but as a re-ar­chi­tec­ture of your data mod­el — be­cause that's ex­act­ly what it is. Auth, stor­age and the raw ex­port are solved prob­lems; plan the data mod­el as the main risk and you plan past the risk. The mon­ey and the time flow into re­shap­ing Fire­store col­lec­tions into nor­malised Post­gres ta­bles, into trans­lat­ing the Se­cu­ri­ty Rules into row lev­el se­cu­ri­ty, and into every­thing that hangs off the Fire­base mo­bile ecosys­tem.

Be­fore any script runs, an hon­est in­ven­to­ry be­longs on the ta­ble: how deep does the app sit in the ecosys­tem, how nest­ed is the data mod­el, is there a strate­gic dri­ver be­yond "Post­gres sounds bet­ter". Come out of that check clean­ly and the mi­gra­tion is one of the sound­est in­vest­ments in tech­no­log­i­cal sov­er­eign­ty you can make. Come out of it murky, and the braver de­ci­sion is not to mi­grate — and to spend the bud­get where it makes a real dif­fer­ence.

Frequently asked questions

Do users have to reset their password after migrating from Firebase to Supabase?
No. Supabase Auth (GoTrue) verifies Firebase scrypt hashes natively through a $fbscrypt$ prefix dispatcher. If you export and import the four Firebase hash parameters correctly (base64_signer_key, base64_salt_separator, rounds, mem_cost), users sign in with their existing password. Take the wrong import path and hashes land with an empty encrypted_password and every login fails — a documented pitfall.
What is the hardest part of a Firebase-to-Supabase migration?
Reshaping the data. Firestore collections are denormalised and duplicated for read performance; Postgres lives on normalised, relationally linked tables. The official guide flattens one collection into one table and dumps nested fields into jsonb. A clean relational model with foreign keys and row level security does not emerge from that automatically — it's manual work and the biggest cost block.
When should you NOT migrate from Firebase to Supabase?
When the app sits deep in the Firebase mobile ecosystem — FCM push, Crashlytics, Remote Config, ML Kit, Analytics — and there's no real sovereignty or cost requirement. These services have no direct Supabase equivalent, so each has to be replaced separately or covered through third parties. Without a strategic driver, the migration burns budget for a feeling instead of a measurable advantage.
Can Firestore realtime with offline sync be carried over to Supabase?
Not one to one. Supabase Realtime streams Postgres WAL changes over WebSockets but offers no native offline persistence and conflict resolution like Firestore's onSnapshot with a local cache. You build the client-side caching yourself, for example with TanStack Query. For many web apps that's a non-issue; for offline-first mobile apps it's a separate implementation block you have to plan in early.

Sources

Related articles

Open for select projects

Let's talk about your project

Book a no-oblig­a­tion call, send us an email, or use the form – we'd love to hear from you.

150+
Completed projects
15
Years of experience
8
Senior‑level team members