Introduction: The Complete Firebase to Supabase Migration Playbook
Migrating from Firebase to Supabase involves replacing Firestore with PostgreSQL, Firebase Auth with GoTrue, Cloud Functions with Edge Functions, and Firebase Storage with S3-compatible storage — all while maintaining zero downtime. By undertaking this transition, organizations can unlock advanced relational querying capabilities, escape vendor lock-in, and leverage the full power of open-source PostgreSQL. This playbook provides a step-by-step engineering blueprint to executing this complex migration efficiently.
Written in September 2026, this guide acts as the hands-on engineering companion to our strategic insights. Before diving into the technical execution, you might want to review the business case in our guide on Cost Savings and Data Sovereignty → or read our platform comparison on Choosing Open-Source Postgres for App Scalability →.
While Supabase is an incredibly powerful Backend-as-a-Service (BaaS) that resolves many limitations of NoSQL databases, it is essential to note when NOT to migrate. If your application heavily relies on offline-first capabilities for mobile apps, requires deep integration within the broader Google Cloud ecosystem (like native BigQuery streams without custom ETLs), or simply runs a very simple document-model architecture that operates well within Firebase's free tier, staying on Firebase might be the pragmatic choice. However, for most scaling SaaS products, the move to Supabase represents a significant leap forward in data integrity and architectural maturity.
Pre-Migration Audit: What to Inventory Before Writing Any Code
Before touching a single line of code or running a data export, you must conduct a comprehensive audit of your existing Firebase architecture. A migration is only as successful as its planning phase. The first step is to catalog every Firebase service your application currently consumes.
| Firebase Service | Audit Requirements |
|---|---|
| Firebase Auth | List all active Auth providers (Email/Password, Google, Apple, etc.), MFA status, and any custom claims usage. |
| Firestore | Document all collections, document count, nesting depth (subcollections), array fields, and indexing requirements. |
| Cloud Functions | Inventory the count of functions, their triggers (HTTP, Firestore onCreate/onUpdate, Pub/Sub, Auth), and external dependencies. |
| Firebase Storage | Calculate total bucket size, number of objects, and any image transformation pipelines in place. |
| FCM & Hosting | Evaluate push notification dependencies and frontend hosting workflows (CI/CD pipelines). |
Beyond the Firebase console, perform a rigorous dependency mapping in your application repositories. Search for any npm packages referencing firebase or firebase-admin, and map out exactly where the Firebase SDK is initialized and utilized. This will highlight the blast radius of your frontend and backend changes.
Finally, conduct a data sovereignty audit. Understand where your data currently resides and where it must reside post-migration. Supabase offers granular region selection, and for strict compliance requirements, self-hosting is an option. If you need assistance with planning your architectural modernization, explore our App Modernization Services → to ensure a compliant and risk-free transition.
Architecture Mapping: Firebase Concepts to Supabase Equivalents
Migrating to Supabase requires translating NoSQL and proprietary serverless concepts into relational paradigms. Supabase is fundamentally built around PostgreSQL, meaning many Firebase services map directly to native Postgres features rather than distinct standalone microservices.
| Firebase Service | Supabase Equivalent | Key Differences & Paradigms |
|---|---|---|
| Firestore / Realtime DB | PostgreSQL | NoSQL documents become relational tables. Subcollections become foreign key relationships. Schemas are strict (though JSONB columns offer flexibility). |
| Firebase Auth | GoTrue (Supabase Auth) | Auth state integrates natively with PostgreSQL schemas. Users live in auth.users schema, allowing direct joins with application data. |
| Cloud Functions | Edge Functions / Database Webhooks / pg_cron | Edge Functions use Deno (not Node.js). Database triggers (plpgsql) handle internal logic synchronously without external HTTP calls. |
| Firebase Storage | Supabase Storage | S3-compatible backend. Built-in image transformations. Policies are managed via SQL (RLS) rather than proprietary rules syntax. |
| Security Rules | Row-Level Security (RLS) | SQL-based policies applied at the database level. More powerful but requires understanding of Postgres execution plans. Tables are OPEN by default. |
| FCM (Push) | None natively | Keep using FCM alongside Supabase, or migrate to OneSignal, Novu, or custom edge functions. |
By internalizing this mapping, your engineering team can conceptually bridge the gap between Google's proprietary ecosystem and standard SQL-based architectures.
Phase 1: Authentication Migration Without Password Resets
A seamless authentication migration is non-negotiable; forcing thousands of active users to reset their passwords causes massive friction and churn. Fortunately, Supabase's GoTrue authentication server natively supports Firebase's modified SCRYPT password hashing algorithm. This means you can export your user base from Firebase and import them directly into Supabase without breaking existing credentials.
First, extract your users using the Firebase Admin SDK. You must request password hash export privileges from Google Cloud Support if your project exceeds a certain size, though the CLI usually handles this for smaller projects. Here is a Node.js snippet for exporting and preparing users for import:
// Node.js snippet for formatting Firebase users for Supabase import
const admin = require('firebase-admin');
admin.initializeApp();
async function exportUsers() {
let nextPageToken;
const allUsers = [];
do {
const listUsersResult = await admin.auth().listUsers(1000, nextPageToken);
listUsersResult.users.forEach((userRecord) => {
// Map to Supabase GoTrue format
allUsers.push({
id: userRecord.uid,
email: userRecord.email,
email_confirmed: userRecord.emailVerified,
phone: userRecord.phoneNumber,
password_hash: userRecord.passwordHash, // SCRYPT hash
password_salt: userRecord.passwordSalt,
raw_app_meta_data: userRecord.customClaims || {},
created_at: new Date(userRecord.metadata.creationTime).toISOString(),
});
});
nextPageToken = listUsersResult.pageToken;
} while (nextPageToken);
// Save to JSON for bulk import via Supabase CLI/API
require('fs').writeFileSync('users.json', JSON.stringify(allUsers));
}
exportUsers();
For Social Logins (OAuth like Google, GitHub, Apple), no re-registration is needed. Supabase supports the same providers, and since the users are mapped by email, the transition is seamless once the OAuth provider credentials (Client ID and Secret) are configured in the Supabase dashboard.
A critical GOTCHA to keep in mind: Multi-Factor Authentication (MFA) enrollments generally cannot be migrated seamlessly. Users who have enabled MFA in Firebase will likely need to re-enroll in Supabase MFA (TOTP). Furthermore, while custom claims map nicely to raw_app_meta_data, you will need to update your application logic to read these claims from the Supabase JWT.
As a rollback plan, implement a parallel auth strategy using a feature flag. Have your frontend attempt login with Supabase first; if it fails (or the flag is disabled), fall back to Firebase Auth. This ensures a safety net during the initial cutover phase.
Phase 2: Database Migration — Firestore to PostgreSQL Schema Design
Migrating from Firestore's NoSQL model to PostgreSQL requires a fundamental paradigm shift in data modeling. In Firestore, data is often denormalized, duplicated, and deeply nested in arrays or subcollections to optimize for reads. In Postgres, you must normalize your data to ensure integrity, utilizing joins and foreign keys.
Here are the core schema transformation patterns:
- Collections → Tables: Top-level collections become standard tables.
- Subcollections → Foreign Keys: A
postssubcollection underusersbecomes apoststable with auser_idforeign key referencing theuserstable. - Arrays → Junction Tables or Arrays: While Postgres supports array columns (e.g.,
text[]), many-to-many relationships (like tags on a post) are better modeled with junction tables for robust querying. - Document Maps → JSONB: If a Firestore document contains highly dynamic, schema-less objects (like user preferences or arbitrary metadata), map this to a
JSONBcolumn in Postgres. - GeoPoints → PostGIS: Convert Firebase GeoPoints to PostGIS geometry types (
POINT(lng, lat)).
Here is an example of an ETL script executing the schema transformation:
// Node.js ETL script transforming Firestore to PostgreSQL via Supabase SDK
const { createClient } = require('@supabase/supabase-js');
const admin = require('firebase-admin');
// Initialize clients...
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY);
async function migrateUsersAndPosts() {
const usersSnapshot = await admin.firestore().collection('users').get();
for (const doc of usersSnapshot.docs) {
const userData = doc.data();
// Insert into Postgres Users table
await supabase.from('users').insert({
id: doc.id,
display_name: userData.displayName,
preferences: userData.preferences || {}, // Stored as JSONB
created_at: userData.createdAt.toDate().toISOString()
});
// Handle subcollections
const postsSnapshot = await doc.ref.collection('posts').get();
for (const postDoc of postsSnapshot.docs) {
const postData = postDoc.data();
await supabase.from('posts').insert({
id: postDoc.id,
user_id: doc.id, // Foreign key linking back to user
content: postData.content,
published: postData.isPublished
});
}
}
}
Crucial Best Practice: Create your tables and relationships first, but do not create complex indexes until the bulk import is complete. Creating indexes before inserting millions of rows will drastically slow down the ingestion process. Insert the data, then run your CREATE INDEX statements.
GOTCHA: Beware of eventual consistency patterns baked into your app. Firestore developers often use complex client-side caching and eventual consistency workarounds. PostgreSQL provides strong consistency. You may find that complex transaction logic in your frontend can be simplified, but your backend queries must be optimized for SQL execution plans.
Phase 3: Security Rules to Row-Level Security (RLS) Policies
Firebase Security Rules use a proprietary, JSON-like syntax to evaluate read/write access. Supabase leverages PostgreSQL's native Row-Level Security (RLS). RLS is significantly more powerful because it operates at the database engine level — meaning even if someone queries the database directly (bypassing the API), the security rules still apply.
Here are four common Firebase Rules converted to Supabase RLS Policies:
1. Owner-Only Access (Users can only read/write their own data)
Firebase: allow read, write: if request.auth.uid == resource.data.userId;
Supabase:
CREATE POLICY "User can manage their own data"
ON public.profiles FOR ALL
USING (auth.uid() = user_id);
2. Organization/Team-Scoped (Multi-tenancy)
Firebase: allow read: if resource.data.orgId in request.auth.token.orgs;
Supabase:
CREATE POLICY "Users can view org data"
ON public.projects FOR SELECT
USING (
org_id IN (
SELECT org_id FROM user_organizations WHERE user_id = auth.uid()
)
);
3. Public Read + Authenticated Write
Firebase: allow read: if true; allow write: if request.auth != null;
Supabase:
CREATE POLICY "Public read access" ON public.posts FOR SELECT USING (true);
CREATE POLICY "Auth write access" ON public.posts FOR INSERT WITH CHECK (auth.role() = 'authenticated');
4. Role-Based Access (Admin/Editor)
Firebase: allow write: if request.auth.token.role == 'admin';
Supabase (using raw_app_meta_data):
CREATE POLICY "Admin write access"
ON public.system_config FOR ALL
USING ( (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin' );
You can test these policies in the SQL Editor using the SET ROLE and SET request.jwt.claim.sub commands to simulate different authenticated users.
GOTCHA: In Firestore, access is DENIED by default. In PostgreSQL, tables are OPEN by default. You must explicitly run ALTER TABLE your_table ENABLE ROW LEVEL SECURITY; on every table. Failing to do so will expose all your data to anyone with the public API key.
GOTCHA: Be cautious with RLS policies containing IN (SELECT...) subqueries on large tables. These can cause massive performance bottlenecks if the queried columns lack proper indexes. Always use EXPLAIN ANALYZE on your queries simulating an authenticated user.
Phase 4: Cloud Functions to Edge Functions and Database Functions
Firebase Cloud Functions handle everything from HTTP webhooks to database triggers. In the Supabase ecosystem, you decompose these responsibilities into specialized, highly performant alternatives.
| Firebase Function Type | Supabase Recommended Target | Reasoning |
|---|---|---|
| HTTP Requests / Webhooks | Edge Functions | Deployed globally to the edge, zero cold starts, fast execution via Deno. |
| Firestore Triggers (onCreate/onUpdate) | PostgreSQL Triggers + Database Webhooks (pg_net) | Native database triggers are synchronous, transactional, and orders of magnitude faster. |
| Auth Triggers (onCreateUser) | Supabase Auth Hooks / DB Triggers on auth.users | Automatically synchronize user creation with public profiles or assign roles instantly. |
| Scheduled / Cron Jobs | pg_cron extension | Run SQL maintenance tasks directly inside the database on a standard cron schedule. |
| Complex Business Logic | PL/pgSQL Functions (RPC) | Execute complex transactions inside the database to eliminate network latency. |
When migrating HTTP functions, you will transition from Node.js (Firebase) to Deno (Supabase Edge Functions). Here is an example of converting a simple Stripe webhook handler.
Firebase (Node.js):
exports.stripeWebhook = functions.https.onRequest(async (req, res) => {
const event = stripe.webhooks.constructEvent(req.rawBody, req.headers['stripe-signature'], secret);
// Process event...
res.status(200).send('OK');
});
Supabase Edge Function (Deno):
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import Stripe from 'https://esm.sh/stripe@11.1.0?target=deno'
serve(async (req) => {
const signature = req.headers.get("Stripe-Signature");
const body = await req.text();
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!);
try {
const event = stripe.webhooks.constructEvent(body, signature!, Deno.env.get('STRIPE_WEBHOOK_SECRET')!);
// Process event...
return new Response("OK", { status: 200 });
} catch (err) {
return new Response(err.message, { status: 400 });
}
})
GOTCHA: Deno is not Node.js. It uses ES modules, requires explicit permission flags (though Supabase handles most of this), and uses standard Web APIs (like fetch and Request/Response objects). You will need to replace Node.js specific libraries (like fs or crypto) with Deno equivalents or use esm.sh to import NPM modules compatible with Deno.
Expert Solutions for Cloud & DevOps
Need help with Cloud & DevOps? Our engineering team builds production-ready solutions tailored to your enterprise workflows.
Phase 5: Storage Migration — Firebase Storage to Supabase Storage
Firebase Storage is backed by Google Cloud Storage. Supabase Storage is an S3-compatible object store tightly integrated with PostgreSQL and RLS. Moving your assets requires a standard download-and-upload pipeline.
The core steps are:
- List all files in your Firebase Storage buckets.
- Download the files securely to an intermediary compute instance.
- Upload the files to Supabase Storage buckets.
- Update all database references (URLs) from Firebase formats to Supabase formats.
- Configure RLS policies on the
storage.objectstable to mirror your previous security rules.
For large buckets, you should utilize an intermediary server or a dedicated worker to stream the files directly to avoid memory constraints.
// Pseudo-code for a streaming storage migration script
const admin = require('firebase-admin');
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
async function migrateStorage() {
const bucket = admin.storage().bucket();
const [files] = await bucket.getFiles();
for (const file of files) {
// Skip directories
if (file.name.endsWith('/')) continue;
// Create a read stream from Firebase
const readStream = file.createReadStream();
const chunks = [];
for await (const chunk of readStream) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
// Upload to Supabase Storage
await supabase.storage
.from('main-bucket')
.upload(file.name, buffer, {
contentType: file.metadata.contentType,
upsert: true
});
console.log(`Migrated ${file.name}`);
}
}
GOTCHA: Firebase leverages Google's global CDN automatically. With Supabase, depending on your plan, you may need to configure a custom CDN (like Cloudflare or AWS CloudFront) in front of your storage buckets to achieve equivalent global latency for large media assets.
GOTCHA: Supabase has highly optimized built-in image transformations (resizing, cropping, formatting) directly in the URL via the Image Transformation API. If you previously maintained a complex Firebase Extension or Cloud Function to generate thumbnails, you can delete that code entirely and utilize Supabase's native URL parameters.
The Parallel-Running Strategy: Zero-Downtime Cutover
A "big bang" cutover — flipping a switch at 2 AM and praying — is an unacceptable risk for production systems. The industry standard for database migrations is the Parallel-Running (or Dual-Write) strategy, which ensures zero downtime and provides an immediate rollback path.
The Dual-Write pattern involves modifying your application backend to write data to both Firebase and Supabase simultaneously, while continuing to read only from Firebase. This ensures that the new Supabase database is kept completely up to date with real-time operations following the initial bulk data ingestion.
The phased rollout looks like this:
- Phase 1: Dual Writes. Deploy backend changes to write to both databases. Handle failures gracefully (if Supabase write fails, log it, but don't fail the Firebase request).
- Phase 2: Data Verification. Run asynchronous scripts comparing the data in Firebase against Supabase to ensure absolute consistency.
- Phase 3: Read Shifts. Utilize a Feature Flag to gradually route read traffic to Supabase. Start with internal team users, then 10% of live traffic, scaling to 50% and finally 100%. Monitor application performance and error rates meticulously.
- Phase 4: Deprecation. Once 100% of reads and writes are securely routed to Supabase, turn off the writes to Firebase. Keep Firebase running in a read-only state for 30 days as a final emergency backup.
During the cutover, you must have robust monitoring metrics in place. Track database connection limits, CPU utilization on your Supabase Postgres instance, and latency percentiles. If Supabase queries underperform compared to Firestore, rollback the read feature flag and analyze your Postgres indexes using EXPLAIN ANALYZE.
Realtime Migration: Firestore Listeners to Supabase Realtime
One of Firestore's strongest selling points is its out-of-the-box realtime listeners (onSnapshot()). Transitioning this functionality requires moving to Supabase Realtime, which leverages PostgreSQL logical replication and websockets to broadcast database changes.
The client-side API conversion is straightforward:
Before (Firestore):
const unsubscribe = db.collection('messages')
.where('chatId', '==', '123')
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added') {
console.log('New message: ', change.doc.data());
}
});
});
After (Supabase):
const channel = supabase
.channel('public:messages')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages', filter: 'chat_id=eq.123' },
(payload) => {
console.log('New message: ', payload.new);
}
)
.subscribe();
| Feature | Firestore Realtime | Supabase Realtime |
|---|---|---|
| Offline Persistence | Native SDK support (caches changes, syncs when online) | Requires custom logic (e.g., WatermelonDB or local storage) |
| Filtering | Complex compound queries supported | Basic equality filters (eq). RLS handles security filtering. |
| Presence | Requires custom implementation via Realtime DB | Built-in Presence API natively supported |
GOTCHA: Supabase Realtime broadcasts changes based on PostgreSQL's replication stream. By default, it does not filter data per user unless you explicitly tie it to Row-Level Security policies or apply strict channel filters. Ensure your replication settings and RLS policies are airtight so users don't accidentally receive a websocket broadcast of another user's private data.
Post-Migration Checklist: 15 Things to Verify Before Going Live
Before turning off your Firebase project, rigorously verify the health and security of your new Supabase deployment. A single misconfiguration can lead to severe performance degradation or data leaks.
- Row-Level Security (RLS) is ENABLED on absolutely every table in the public schema.
- Auth verification: Ensure newly imported users can log in using their old passwords without errors.
- Referential Integrity: Confirm that all foreign keys have
ON DELETE CASCADEorRESTRICTlogic properly configured to avoid orphaned records. - Indexes are optimized: Run
EXPLAIN ANALYZEon your top 5 most frequent queries. Ensure B-Tree indexes exist on all foreign key columns. - Connection Pooling: Verify PgBouncer (or Supabase Supavisor) is active and your application is connecting via the pooling URL (port 6543) rather than the direct database port.
- Storage Policies: Confirm that private buckets cannot be accessed via public URLs and RLS on
storage.objectsis functioning. - Edge Functions Health: Monitor Deno execution logs to ensure there are no unhandled Promise rejections or memory leaks.
- Point-in-Time Recovery (PITR): Verify that your Supabase compute plan includes automated daily backups and PITR is enabled for enterprise safety.
- Rate Limiting: Implement API rate limiting on your Edge Functions to prevent DDOS or runaway client loops.
- Database Triggers: Check that background triggers (like auto-updating
updated_attimestamps) fire correctly onUPDATE. - Load Testing: Simulate peak traffic against your staging Supabase instance using tools like k6 or Artillery.
- Custom Domains & CDN: Ensure your Supabase API and Storage endpoints are routed through your custom domain and caching layer.
- FCM / Notifications: Verify your external push notification service is correctly tied to your new Edge Functions workflow.
- Disaster Recovery Plan: Document the exact steps required to restore the Supabase database from a snapshot in case of catastrophic failure.
- Team Training: Ensure your frontend and backend engineers understand PostgreSQL paradigms and the Supabase dashboard tools.
For more insights on moving fast and efficiently with your new architecture, check out our guide on how to Launch Your SaaS MVP in 6 Weeks with Supabase →.
Common Migration Mistakes and How to Avoid Them
Over the course of facilitating numerous enterprise migrations, we have identified recurring pitfalls that engineering teams stumble into when transitioning from Firebase.
- 1:1 Schema Copying Without Normalizing: Simply copying heavily nested NoSQL documents into giant JSONB columns in Postgres negates the primary benefit of migrating. You must normalize your data into relational tables.
- Forgetting to Enable RLS: As mentioned, Postgres tables are open by default. Failing to enable RLS is the fastest way to suffer a massive data breach.
- Not Testing with Production-Scale Data: Queries that execute in 10ms on an empty staging database might take 10 seconds when executing a sequential scan against millions of rows. Always test with realistic data volumes.
- Ignoring Offline-First Requirements: Supabase lacks Firebase's robust out-of-the-box offline caching and synchronization SDK. If your mobile app relies heavily on this, you must engineer a local cache solution (like WatermelonDB or SQLite).
- Underestimating Deno vs Node.js: Attempting to copy-paste Node.js Cloud Functions directly into Edge Functions will fail. Account for the time needed to refactor to Deno and modern ES Modules.
- No Connection Pooling (Exhaustion): Serverless environments (like Vercel or AWS Lambda) can rapidly open thousands of database connections, overwhelming Postgres. Always use Supabase's built-in connection pooler (Supavisor).
- Assuming Realtime = Firestore Listeners: Supabase Realtime is incredibly powerful, but it handles state and offline buffering differently. It is not a 1:1 drop-in replacement.
- Skipping the Parallel-Running Phase: Trying to execute a "Big Bang" migration over a weekend almost always results in user-facing downtime and lost data. Dual-writes are mandatory for safety.
If your team is facing architectural complexity, it might be time to bring in the experts. Read our guide on When Your Supabase Project Needs Senior-Level Consulting → to understand how expert oversight can derisk your migration.
Ready to Migrate from Firebase to Supabase?
Executing a zero-downtime database migration requires precision, deep PostgreSQL expertise, and battle-tested operational playbooks. The MetaDesign Solutions Cloud & DevOps Engineering team has successfully architected and executed over 15 production migrations from Firebase to Supabase for high-growth SaaS platforms and enterprise applications.
Stop wrestling with NoSQL limitations and vendor lock-in. Partner with our engineering experts to safely transition your architecture to the world's most advanced open-source database platform.
Learn more about our App Modernization Services → to see how we tackle complex architectural challenges, or simply Contact Us → today to schedule a technical discovery call with our migration architects.





