I Migrated a 50k-User App from Firebase to Supabase in Two Weeks - Here's What I Learned
Codoric Team

I Migrated a 50k-User App from Firebase to Supabase in Two Weeks - Here's What I Learned
A client came to me with a fairly common problem: their app had grown past the point where Firestore's pricing and query limitations made sense, but Firebase Auth and Firestore were wired into nearly every screen. They wanted out - without a rewrite, without downtime, and without losing a single user's data. Two weeks later, the app was running fully on Supabase. This is the honest version of how that went, including the parts that almost went wrong.
The starting point
The app was a mid-size B2C product: roughly 50,000 registered users, Firestore as the primary database, Firebase Auth for login (email/password + Google), and a handful of Cloud Functions doing background work (email notifications, usage aggregation). Nothing exotic - which is exactly why the migration was worth documenting. Most teams in this position have the same shape of problem.
The trigger for the migration wasn't a single blocker. It was a combination of:
- Firestore's document-based queries making basic reporting (joins, aggregates) painful and expensive
- Read/write costs climbing faster than the user base
- No easy way to enforce row-level data rules without duplicating logic in every Cloud Function
- A team that wanted plain SQL and Postgres tooling they already knew from other projects
None of that is Firebase-specific criticism - Firestore is a fine choice for a lot of apps. It just wasn't the right fit anymore for this one.
Step 1: Mapping Firestore collections to Postgres tables
This is where most migration estimates go wrong. Firestore is schemaless and nested by design - a users collection with subcollections for orders, preferences, and activity doesn't translate 1:1 into relational tables.
The real work here wasn't writing SQL - it was deciding on the shape of the new schema before touching any data:
create table public.profiles (
id uuid references auth.users(id) primary key,
display_name text,
created_at timestamptz default now()
);
create table public.orders (
id uuid default gen_random_uuid() primary key,
user_id uuid references public.profiles(id) not null,
status text not null,
total_cents integer not null,
created_at timestamptz default now()
);
The subcollections became foreign-key relationships. Denormalized fields that existed in Firestore purely to avoid extra reads (a classic Firestore pattern) got removed - Postgres joins made them unnecessary, and keeping them would have just been a source of drift.
Lesson: budget more time for schema design than for the actual data transfer script. Getting the relational model wrong here means rewriting migration scripts twice.
Step 2: Moving the data
A one-off Node.js script pulled every document out of Firestore via the Admin SDK, transformed it into the new relational shape, and inserted it into Supabase using the supabase-js service role client. Nothing fancy - batched reads, batched inserts, and a lot of console.log to track progress against 50k users' worth of records.
Two things mattered more than the script itself:
- A dry run against a Supabase staging project first. Not a local Postgres instance - the actual hosted staging project, because connection pooling and RLS behave differently than a local
psqlsession. - Idempotency. The script could be re-run safely if it crashed halfway, because inserts checked for existing rows by the original Firestore document ID (kept temporarily as a
legacy_idcolumn, dropped after verification).
Step 3: The auth cutover - the part that almost broke launch day
This was the riskiest step, and it's the one most write-ups skip over. Firebase Auth and Supabase Auth both issue JWTs, but the user IDs are different formats, and existing users' sessions were live in production.
The plan: keep Firebase Auth as the identity source during a transition window, mint corresponding Supabase users via the Admin API mapped by email, and force a one-time silent re-authentication on next app open - not a forced logout, just a background token exchange so users never noticed.
The near-miss: password hashes. Firebase does not export password hashes in a format Supabase (or any other provider) can import directly, and this is genuinely one of the least-documented gaps in any Firebase migration. There is no clean way around it. The options are:
- Force a password reset email to every user (disruptive, kills conversion on re-login)
- Keep Firebase Auth running in parallel purely for password verification during a grace period, then migrate the user to Supabase Auth the moment they successfully log in
We went with the second option. It meant running two auth systems side by side for about ten days, which added complexity but caused zero user-facing disruption. Users who never logged back in during that window got a one-time "reset your password" email at the end of the grace period - a small number, and expected.
Step 4: Rewriting the data access layer
Every Firestore onSnapshot listener and .get() call had to become a Supabase query or a realtime subscription. The API surface is different enough that this wasn't a find-and-replace - it was a genuine rewrite of the data layer, screen by screen.
The upside: a lot of client-side logic that existed only to work around Firestore's query limitations (filtering and sorting done in JavaScript because Firestore couldn't express the query) simply disappeared. Postgres could express it directly.
What I'd do differently next time
- Start the auth mapping earlier. It ended up being the long pole in the schedule, not the data migration.
- Write the RLS policies before the frontend rewrite, not after. We bolted Row Level Security on near the end, which meant re-testing screens that had already been "done."
- Keep the
legacy_idcolumns around longer than felt necessary. They were the safety net for reconciling data discrepancies discovered a week after the cutover, when a handful of users reported missing order history caused by a timezone bug in the transform script.
Was it worth it?
For this app, yes - query costs dropped, reporting that used to require exporting data to BigQuery could now run as a plain SQL query, and the team could use tools (pgAdmin, standard ORMs) they already knew. That won't be true for every app; Firestore is still a reasonable default for apps with simple, document-shaped data and no reporting needs. But if you're hitting the same wall - awkward queries, rising costs, wanting Row Level Security instead of duplicating rules in every Cloud Function - the migration path above is a realistic two-week timeline, not the multi-month rewrite it's often assumed to be.
If you're mid-way through a similar Firebase-to-Supabase migration and stuck on the auth cutover or the schema design, that's the exact kind of problem I help teams work through - feel free to book a call and we can talk through your specific setup.