Migrating from Firebase to Self-Hosted Supabase: A Complete Guide

Move your app from Firebase to self-hosted Supabase: export Auth users with password hashes, convert Firestore to Postgres, and migrate Storage files.

Cover Image for Migrating from Firebase to Self-Hosted Supabase: A Complete Guide

Leaving Firebase is rarely about disliking Firebase. It's about the bill that scales with reads, the queries you can't write in Firestore, and the fact that your data lives in a proprietary format you can't take with you easily. Migrating to self-hosted Supabase fixes all three at once — you get real Postgres, real SQL, and infrastructure you own — but the migration itself has sharp edges that the official cloud-focused guides gloss over. This guide covers the full path: exporting Auth users with their password hashes intact, flattening Firestore collections into relational tables, moving Storage files, and the self-hosted-specific configuration you'll need along the way.

If you're still weighing the decision, read our Firebase vs Supabase self-hosting comparison first. If you've decided — let's migrate.

Before You Start: Get Supabase Running

You need a working self-hosted Supabase instance before importing anything. Follow our step-by-step server deployment guide or the installation docs to get the stack up. Two things worth knowing in 2026:

  • New self-hosted deployments default to Postgres 17 (the default image moved from PG15 in June 2026), so you're starting on a current major version.
  • The analytics and vector services are now opt-in, which means a leaner default stack — good news if you're sizing a first VPS.

Verify Auth, the REST API, and Storage all respond before touching Firebase. Debugging a half-broken import against a half-broken stack is misery.

Step 1: Migrate Auth Users (With Passwords)

This is the part everyone gets wrong. The common claim is that Firebase won't give you password hashes, so you must force every user through a reset flow. That's not quite true — and the difference matters, because a forced-reset email to your entire user base is where migrations go to die.

Firebase uses a modified scrypt algorithm, and the Firebase CLI will export the hashes:

firebase auth:export users.json --format=json --project your-project-id

Each record includes a base64 passwordHash and salt. The hash parameters — signer key, salt separator, rounds, and memory cost — are project-level values you'll find in the Firebase Console under Authentication → Users → Password hash parameters (three-dot menu). You need project Owner permissions to see them. Save all four.

Here's the payoff: Supabase Auth (GoTrue) natively supports Firebase's scrypt variant via the $fbscrypt$ hash format. That means you can insert users with a hash string like:

$fbscrypt$v=1,n=14,r=8,p=1,ss=Bw==,sk=<signer-key>$<salt>$<hash>

where n is your project's memory cost (as a power of two), r is rounds, ss is the salt separator, and sk is the signer key. Users sign in with their existing password on day one, GoTrue verifies it against the Firebase hash, and no reset email is ever sent.

A minimal import script against your self-hosted instance using the Admin API:

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  'https://api.yourdomain.com',
  process.env.SERVICE_ROLE_KEY,
  { auth: { autoRefreshToken: false, persistSession: false } }
);

for (const user of firebaseUsers) {
  await supabase.auth.admin.createUser({
    email: user.email,
    email_confirm: user.emailVerified,
    password_hash: buildFbscryptHash(user), // $fbscrypt$ format above
    user_metadata: { firebase_uid: user.localId, name: user.displayName },
  });
}

Three self-hosted-specific gotchas:

  1. Keep the Firebase UID. Store it in user_metadata (or a mapping table). Every Firestore document that references a user does so by Firebase UID, and you'll need the mapping in Step 2.
  2. OAuth users have no password hash. Google/Apple/GitHub sign-in users just need matching providers configured on your instance. Set those up in the auth providers configuration before go-live, and make sure the redirect URLs point at your domain, not Firebase's.
  3. Configure SMTP first. Self-hosted Supabase ships with no working mail transport. Confirmation and recovery emails silently go nowhere until you set up SMTP and email templates — do it before the import, not after the first support ticket.

Step 2: Firestore Data → Postgres

This is less a data migration than a paradigm migration. Firestore is a document store; Postgres is relational. A users collection with embedded arrays of orders becomes a profiles table and an orders table joined by a foreign key.

Supabase's open-source firestore2json tooling copies one Firestore collection into one Postgres table, flattening fields into text, numeric, boolean, or jsonb columns. It works against your self-hosted database — just point it at your Postgres connection string instead of a cloud project.

The pragmatic strategy that works for most teams:

Land it, then normalize it. Import each collection as-is, letting nested maps and arrays fall into jsonb columns. Your data is now safe in Postgres and queryable immediately — Postgres handles JSONB remarkably well, with indexing and operators Firestore can't match. Then normalize incrementally:

-- Extract embedded orders from the landed jsonb into a real table
INSERT INTO orders (profile_id, item, amount, created_at)
SELECT
  p.id,
  o->>'item',
  (o->>'amount')::numeric,
  to_timestamp((o->>'createdAt')::bigint / 1000)
FROM profiles_raw p,
     jsonb_array_elements(p.data->'orders') AS o;

Two conversion traps: Firestore timestamps export as epoch milliseconds or {_seconds, _nanoseconds} objects depending on the export path — normalize them to timestamptz on the way in. And Firestore document references export as path strings (users/abc123); split out the ID and join it through your UID mapping table to get real foreign keys.

Finally, write Row Level Security policies before exposing anything. Firestore security rules do not translate automatically, and self-hosted PostgREST will happily serve any table in an exposed schema. A firebase_uid column plus a policy like auth.uid() = profile_id replicates the typical request.auth.uid rule. Skipping this step is the single most dangerous mistake in the whole migration.

Step 3: Storage Files

Firebase Storage is a GCS bucket, so gsutil/gcloud storage can pull everything down (or transfer bucket-to-bucket if you back Supabase Storage with S3-compatible object storage rather than local disk):

gcloud storage cp -r gs://your-project.appspot.com/ ./firebase-export/

Then upload through the Supabase Storage API so the storage.objects metadata rows are created properly — copying files straight into the backend bucket leaves Storage unaware they exist. Preserve your path structure where you can, since paths are usually referenced from Firestore documents you've just imported. And remember that Storage needs its own RLS policies too, plus its own backup story — files aren't included in database dumps.

One honest caveat: if you use Firebase Cloud Messaging, you don't have to give it up. FCM works fine alongside self-hosted Supabase — see our push notifications with FCM guide.

Step 4: Cut Over Carefully

Don't big-bang it. The sequence that avoids 2 a.m. surprises:

  1. Dry-run the full import against a staging instance. Time it — Auth imports run one Admin API call per user, so 100k users takes hours, not minutes.
  2. Freeze writes on Firebase (maintenance mode or a read-only flag).
  3. Run the final sync: delta export of Auth, Firestore, and Storage since the dry run.
  4. Flip the client. Swap the Firebase SDK for supabase-js, point it at your domain, deploy.
  5. Keep Firebase alive for 30 days as a read-only fallback. It costs almost nothing without traffic, and it's your escape hatch.

And take a backup the moment the import finishes — that pristine post-migration snapshot is the restore point you'll want if a normalization script goes sideways in week two. Set up automated backups immediately, including Storage.

Where Supascale Fits

Everything above is doable by hand. But you've just traded Firebase's managed platform for infrastructure that's now your responsibility — and the operational gaps (no backup scheduler, no domain/SSL automation, OAuth config buried in env vars) are exactly what Supascale covers: automated S3 backups with one-click restore that include Storage files, custom domains with free SSL so your API lives at api.yourdomain.com from day one, and a UI for configuring the Google/Apple/GitHub providers your migrated OAuth users depend on. It's a one-time license from $99 with no per-project or per-seat fees — the pricing model you presumably just left Firebase to find.

The Takeaway

A Firebase → self-hosted Supabase migration is four distinct migrations: Auth (easier than reputed — the $fbscrypt$ format preserves passwords), Firestore data (land in JSONB, normalize incrementally), Storage (export via gsutil, re-upload through the API), and security rules (rewrite as RLS, before go-live). None of it is exotic, but the ordering matters: stack first, SMTP and OAuth providers second, users third, data fourth, backups immediately after. Do it in that order and your users won't even notice the ground moved beneath them.

Further Reading