· supabase / firebase / migration

How to Migrate Firebase to Supabase: A Step-by-Step Guide

Migrate Firebase to Supabase without losing Auth, Firestore, or Storage — the right order, 8 blockers to know, and the Analytics gap you need to plan for.

By · Updated May 20, 2026

2,033 words · 11 min read

You can migrate Firebase to Supabase without losing auth sessions, Firestore data, or Storage assets — but only if you run steps in the right order and know which parts have no equivalent. This guide walks through Auth, Firestore, and Storage migrations using the firebase-to-supabase toolkit (153 ⭐, updated 2026-05-15), with before/after SDK snippets for all four service areas and the full Firestore migration script.

One gap you cannot close: Firebase Analytics has no Supabase equivalent. If you depend on Firebase Analytics, migrate that data out to a separate analytics service before you touch anything else.

Who this is for

Developers running Firebase Blaze plan who want to move to a SQL-first, self-hostable backend. If you’re on Firebase Spark (free tier) and not hitting limits, this migration costs real time for unclear short-term gain — come back when billing starts to hurt.

Firebase vs Supabase: service mapping at a glance

Firebase serviceSupabase equivalentMigration effort
Firebase AuthSupabase AuthMedium — Scrypt hashes are preserved, but UUID format changes
FirestorePostgresHigh — document collections must be designed into relational tables
Realtime DatabaseSupabase RealtimeMedium — pub/sub model is similar; connection limits differ
Cloud StorageSupabase StorageLow — download/upload scripts handle the transfer
Cloud FunctionsEdge FunctionsMedium — runtime differences; most functions port cleanly
Firebase Analytics❌ No equivalentPlan a separate migration to Mixpanel, PostHog, or similar

If you’re still weighing the decision, Supabase vs Firebase (2026) covers the full cost model and feature trade-offs in detail.

Before you migrate: planning your schema

The hardest part of this migration is Firestore, not Auth or Storage. Collections of documents with arbitrary nesting do not map cleanly to Postgres tables. Schema design happens before you write a single migration script, and getting it wrong means running the migration twice.

For each Firestore collection:

  1. List every field and its type.
  2. Identify nested objects and arrays. Decide: separate table with a foreign key, or JSONB column?
  3. Decide on your Postgres primary key strategy. Firebase document IDs are arbitrary strings. Supabase defaults to UUIDs. You can keep string IDs as a firebase_id column for reference during the transition, but your application code will need to switch to UUID lookups eventually.

Write these decisions down. You’ll need them when running firestore2json.js. Once Firestore is in Postgres, you’ll also want a query layer — our best TypeScript ORM roundup compares Drizzle, Prisma, and Kysely so you can pick before you write the first query.

Migrating Firebase Auth to Supabase

Supabase’s Auth migration preserves Scrypt password hashes, so existing users do not need to reset passwords. The official migration guide walks through this.

Step 1: Export Firebase users

firebase auth:export users.json --format=json

This writes all user records — including the Scrypt hash parameters — to users.json. For projects with more than 1 million users, add --limit and loop in batches.

Step 2: Transform and import via the toolkit

npx github:supabase-community/firebase-to-supabase/auth

The toolkit reads users.json, maps Firebase UID to a UUID (maintaining a lookup table you’ll need later for Firestore), and calls the Supabase Admin API to bulk-insert users. Scrypt hash parameters are passed through the raw_app_meta_data column, so authentication continues to work for users who haven’t logged in since the migration.

Failure mode: If a user’s email already exists in Supabase (from a prior partial import), the import skips that row without erroring loudly. Check the output log’s skipped count against your Firebase user total before proceeding.

Step 3: Re-configure Google Sign-In

Google OAuth requires re-setup in the Supabase dashboard under Authentication → Providers → Google. The client ID and secret from your Firebase project can be reused — these are tied to your Google Cloud project, not to Firebase itself.

Migrating Firestore data to Postgres

The firebase-to-supabase toolkit provides a three-script pipeline for Firestore migration. Install it once and run the three phases in order.

git clone https://github.com/supabase-community/firebase-to-supabase
cd firebase-to-supabase
npm install

Phase 1: List your collections

node firestore/collections.js

Output is a list of top-level collection names. Review this — collections nested under documents (sub-collections) do not appear here and require separate handling.

Phase 2: Export Firestore to JSON

node firestore/firestore2json.js <collection-name> <output-file.json>

Run once per collection. For large collections, add --limit and paginate. The output includes Firestore document IDs as _id fields.

Phase 3: Import JSON to Postgres

node firestore/json2supabase.js <output-file.json> \
  --supabaseUrl=<your-project-url> \
  --supabaseKey=<service-role-key> \
  --tableName=<target-table>

The tool creates the target table if it doesn’t exist, inferring column types from the JSON. For nested objects, it defaults to JSONB. You can override with a --schema flag pointing to a JSON schema file.

Full migration script for a users collection:

import { createClient } from '@supabase/supabase-js';
import * as admin from 'firebase-admin';
import * as fs from 'fs';

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

admin.initializeApp({
  credential: admin.credential.applicationDefault(),
});

const db = admin.firestore();

async function migrateCollection(collectionName: string) {
  const snapshot = await db.collection(collectionName).get();
  const rows = snapshot.docs.map((doc) => ({
    id: crypto.randomUUID(), // new UUID for Supabase
    firebase_id: doc.id,    // preserve original for lookup
    ...doc.data(),
    created_at: doc.data().createdAt?.toDate?.() ?? new Date(),
  }));

  const { error } = await supabase.from(collectionName).insert(rows);

  if (error) {
    console.error(`Failed on ${collectionName}:`, error.message);
    process.exit(1);
  }

  console.log(`Migrated ${rows.length} records from ${collectionName}`);
}

await migrateCollection('users');
await migrateCollection('posts');

Failure mode: Firestore Timestamps serialize to { _seconds, _nanoseconds } objects, not ISO strings. The script above calls .toDate() explicitly. If you skip this, you get JSONB blobs where you expected timestamptz columns.

Migrating Firebase Storage to Supabase Storage

Storage migration is the lowest-effort part. The toolkit provides two scripts: one to download Firebase Storage files locally, one to upload them to a Supabase bucket.

Step 1: Download from Firebase Storage

node storage/download.js \
  --firebaseBucket=<your-project>.appspot.com \
  --localPath=./storage-backup

For large buckets, this runs for a while. The local path mirrors the remote directory structure.

Step 2: Upload to Supabase Storage

node storage/upload.js \
  --supabaseUrl=<your-project-url> \
  --supabaseKey=<service-role-key> \
  --bucketName=<bucket> \
  --localPath=./storage-backup

Public vs. private bucket settings are independent of Firebase’s access rules — set these in the Supabase dashboard and update your RLS policies accordingly.

Failure mode: Firebase Storage paths that begin with a dot (.) or contain consecutive slashes (//) may fail the Supabase upload. Scan for these with find ./storage-backup -name ".*" before uploading.

Updating your client SDK

Once data is migrated, update your application code. Here are before/after snippets for all four service areas.

Auth: sign in

// Firebase
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';
const auth = getAuth();
const { user } = await signInWithEmailAndPassword(auth, email, password);

// Supabase
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(url, anonKey);
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
const user = data.user;

Database: read and write

// Firebase — read
import { getFirestore, doc, getDoc } from 'firebase/firestore';
const db = getFirestore();
const snap = await getDoc(doc(db, 'posts', postId));
const post = snap.data();

// Supabase — read
const { data: post } = await supabase
  .from('posts')
  .select('*')
  .eq('id', postId)
  .single();

// Firebase — write
import { setDoc } from 'firebase/firestore';
await setDoc(doc(db, 'posts', postId), { title, body });

// Supabase — write
await supabase.from('posts').upsert({ id: postId, title, body });

Storage: upload

// Firebase
import { getStorage, ref, uploadBytes } from 'firebase/storage';
const storage = getStorage();
await uploadBytes(ref(storage, `uploads/${filename}`), file);

// Supabase
await supabase.storage
  .from('uploads')
  .upload(filename, file, { upsert: true });

Realtime: subscribe

// Firebase
import { getDatabase, ref, onValue } from 'firebase/database';
const db = getDatabase();
onValue(ref(db, `rooms/${roomId}`), (snap) => {
  console.log(snap.val());
});

// Supabase
supabase
  .channel(`room:${roomId}`)
  .on('postgres_changes', { event: '*', schema: 'public', table: 'rooms', filter: `id=eq.${roomId}` }, (payload) => {
    console.log(payload.new);
  })
  .subscribe();

Note the model difference: Supabase Realtime subscribes to Postgres row changes, not arbitrary JSON paths. This means your Realtime subscriptions are always scoped to a table and can be filtered with Postgres conditions — more structured than Firebase Realtime Database, which lets you subscribe to any path.

Pricing comparison: will Supabase save you money?

Firebase BlazeSupabase Pro
Base price$0 (pay-as-you-go)$25/month
AuthFree up to 50K MAU100K MAU included, then $0.00325/MAU
Database$0.06/GB/month (Firestore)8 GB included, $0.125/GB after
Storage$0.026/GB/month100 GB included, $0.021/GB after
Bandwidth$0.12/GB250 GB included, $0.09/GB after
Realtime connectionsSpark: 100; Blaze: 200K per database500 concurrent on Pro

At ~30,000–50,000 monthly active users, Supabase Pro typically undercuts Firebase Blaze on total monthly cost. Below that range, Firebase’s pay-as-you-go pricing is often cheaper. Above 500 concurrent Realtime connections, you need Supabase’s Team or Enterprise plan.

Supabase Pro also includes daily backups, branching, and custom domains — items that cost extra or require workarounds on Firebase.

Gotchas and blockers to know before you start

1. Auth UUID mismatch. Firebase UIDs are strings like abc123. Supabase UIDs are UUIDs. Every table that has a user_id foreign key needs to be updated after auth migration. The toolkit creates a firebase_uid_to_supabase_uid mapping table for this — use it before removing the firebase_id columns.

2. Timestamp format. Firestore Timestamps serialize to { _seconds, _nanoseconds } objects in JSON export. Call .toDate() before inserting into Postgres. Skip this and you’ll insert JSONB blobs into timestamptz columns — Postgres won’t complain at insert time, but queries will fail.

3. Nested document flattening. Firestore documents can contain sub-collections that don’t appear in the top-level export. Run collections.js and look for sub-collections manually. Flattening these into Postgres requires designing the join tables before migration, not after.

4. Realtime connection limits. Firebase Realtime Database on Blaze caps at 200K concurrent connections per database. Supabase Pro caps at 500 concurrent connections. If your peak exceeds this, upgrade to Team plan before going live, or implement connection pooling client-side.

5. No Firebase Analytics equivalent. This is a hard gap. Plan your analytics migration to a third-party service (Mixpanel, PostHog, Amplitude) as a separate project. Migrate Firebase Analytics data first; don’t let it become a blocker on the rest of the migration.

6. Google Sign-In requires re-setup. The OAuth client ID and secret carry over, but you must re-configure the Authorized redirect URIs in Google Cloud Console to point to your Supabase project’s Auth callback URL. Existing Google Sign-In users will get a re-consent prompt if the redirect URI changes.

7. No Firestore-style transactions. Firestore supports multi-document transactions across collections. Postgres uses standard SQL transactions, but the Supabase client library’s rpc() method calls stored functions — use Postgres stored procedures or functions for multi-table atomic operations. Rewriting transaction logic to stored functions is the most time-consuming part of complex Firestore migrations.

8. RLS is off by default. Supabase creates tables with Row Level Security disabled. Every table needs explicit RLS policies before you can rely on the anon key client-side. Missing this is a security risk, not a functionality risk — the app will work, but every row is publicly readable. Enable RLS on every table immediately after creating it:

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can read own posts"
  ON posts FOR SELECT
  USING (auth.uid() = user_id);

Migrate in this order

  1. Firebase Analytics → external analytics (parallel project, start first)
  2. Firebase Auth → Supabase Auth (users, hashes, OAuth re-setup)
  3. Firestore → Postgres (schema design, then three-phase script)
  4. Cloud Storage → Supabase Storage (download → upload)
  5. Client SDK → swap imports, update foreign key references
  6. RLS policies → enable before switching production traffic

Rushing steps 3–5 before step 2 is complete means your user_id references point at Firebase UIDs that don’t exist in Supabase yet. Do Auth first.

References