How to Migrate from Firebase to Supabase: Complete Step-by-Step Guide
🔍 Want the best deal? Check current prices and availability.
Compare Prices →Disclosure: When you buy through links on our site, we may earn a commission. This helps us keep reviews honest and independent.
Firebase is great for prototyping, but once your app starts scaling, the pain points become real: vendor lock-in, unpredictable pricing (especially Firestore reads/writes), and a closed-source ecosystem. Supabase offers a compelling alternative—open-source PostgreSQL, real‑time subscriptions, a generous free tier, and the freedom to self‑host if needed.
I recently migrated a production app with ~5k users from Firebase to Supabase. This guide walks you through every step I took, including the gotchas I hit along the way. By the end, you’ll have a clear path to move your database, authentication, and storage—without downtime.
What You’ll Need
Before we start, Make sure you have:
- An existing Firebase project with Firestore, Auth, and Cloud Storage enabled.
- A Supabase project – you can create one at supabase.com (the free tier is plenty for small to medium apps).
- Node.js 18+ and
npminstalled locally. - The Firebase CLI:
npm install -g firebase-tools - The Supabase CLI:
npm install -g supabase(or use the dashboard).
You’ll also need administrative access to both projects (Firebase Owner role, Supabase project owner).
Step 1: Export Your Firebase Data
We’ll export data in three parts: Firestore documents, Auth users, and Storage files.
1a. Export Firestore to JSON
Use the Firebase Emulator or the firestore:export command. The cleanest approach is to use firebase-tools:
firebase export --project your-project-id --output ./firebase-export
This creates a structured directory with all collections in newline‑delimited JSON. If you only need certain collections, you can filter later with a script.
Pain point: Firestore stores nested objects and references that don’t map 1:1 to SQL. You’ll need to flatten complex structures later.
1b. Export Auth Users
Firebase doesn’t have a direct export for Auth users. Use the Admin SDK to list them:
const admin = require('firebase-admin');
const fs = require('fs');
admin.initializeApp({ credential: admin.credential.applicationDefault() });
async function exportUsers() {
const users = [];
const listAll = async (nextPageToken) => {
const result = await admin.auth().listUsers(1000, nextPageToken);
result.users.forEach(u => users.push(u.toJSON()));
if (result.pageToken) await listAll(result.pageToken);
};
await listAll();
fs.writeFileSync('./firebase-auth-users.json', JSON.stringify(users, null, 2));
}
exportUsers();
Save this as export-auth.js and run node export-auth.js. You’ll get a JSON array of user objects including UID, email, display name, and hashed password (Firebase stores passwords as bcrypt hashes – more on that in Step 4).
1c. Download Storage Files
Use the Firebase Storage emulator or gsutil:
# List all buckets
gsutil ls gs://your-project-id.appspot.com
Download recursively
gsutil -m cp -r gs://your-project-id.appspot.com/folder ./local-backup/
If your app stores files in gs://bucket/user-uploads/, download them to a local folder. This can be slow with many files – consider using rsync if you’re on a server.
Step 2: Set Up Your Supabase Project
Log in to your Supabase dashboard and create a new project. Choose a region close to your users (same as your Firebase location if possible).
2a. Schema Design
Firestore schemas are flexible; PostgreSQL requires defined tables. Map your collections:
| Firestore Collection | Supabase Table | Notes |
|---|---|---|
users | profiles | Use auth.users for authentication data. |
posts | posts | Flatten nested arrays into junction tables. |
user_activities | user_activities | Timestamps → timestamptz columns. |
For example, a Firestore document like:
{
"name": "Alice",
"likedPosts": ["post1", "post2"],
"profile": { "bio": "Dev", "age": 30 }
}
Becomes two Supabase tables: profiles(id, name, bio, age) and a join table profile_liked_posts(profile_id, post_id).
Use the Supabase SQL editor to create tables. I recommend enabling Row Level Security (RLS) from the start – it’s as powerful as Firestore security rules but more familiar to SQL developers.
2b. Enable Extensions
Supabase ships with many PostgreSQL extensions. For real‑time features, enable pg_cron and pg_net if you need scheduled tasks. For full‑text search, run:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
Make a note of your Supabase project’s connection string (we’ll use it in the next step).
Step 3: Import Data into Supabase
The cleanest way to migrate data is to write a Node.js script that reads your Firestore export and inserts into Supabase using pg or knex.
3a. Prepare the Data
Firestore exports use a specific internal format. I wrote a parser that flattens documents:
// parse-firestore-export.js
const fs = require('fs');
const raw = JSON.parse(fs.readFileSync('./firebase-export/your-collection.json', 'utf8'));
const documents = raw.documents.map(doc => {
const fields = doc.fields;
const flat = { id: doc.name.split('/').pop() };
for (const [key, value] of Object.entries(fields)) {
// Map Firestore types to JS values
if (value.stringValue !== undefined) flat[key] = value.stringValue;
else if (value.integerValue !== undefined) flat[key] = parseInt(value.integerValue, 10);
else if (value.timestampValue !== undefined) flat[key] = new Date(value.timestampValue);
// handle arrays, maps, etc.
}
return flat;
});
fs.writeFileSync('./parsed-collection.json', JSON.stringify(documents, null, 2));
Run it for each collection you need.
3b. Insert into Supabase
Use the Supabase JavaScript client or raw SQL. For large datasets, I prefer pg with batch inserts:
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.SUPABASE_DB_URL });
async function insertProfiles(profiles) {
const client = await pool.connect();
try {
const batchSize = 500;
for (let i = 0; i < profiles.length; i += batchSize) {
const batch = profiles.slice(i, i + batchSize);
const values = batch.map((p, idx) =>
($${idx 3 + 1}, $${idx 3 + 2}, $${idx * 3 + 3})
).join(',');
const params = batch.flatMap(p => [p.id, p.name, p.email]);
await client.query(
INSERT INTO profiles (id, name, email) VALUES ${values} ON CONFLICT (id) DO NOTHING,
params
);
}
} finally {
client.release();
}
}
insertProfiles(parsedProfiles);
Gotcha: Firestore timestamps are in nanosecond resolution. PostgreSQLtimestamptzaccepts microsecond precision – convert withMath.floor(timestampNs / 1000).
3c. Migrate Real‑time Data (Optional)
If your app relies on Firestore real‑time listeners, Supabase offers equivalent subscriptions via supabase-js. You’ll need to rewrite client code, but the database triggers and replication are similar.
Step 4: Migrate Authentication Users
Firebase stores user credentials (email/password) as bcrypt hashes, but the algorithm version and salt are specific to Firebase. You cannot directly import the hash into Supabase’s auth.users table (which uses a different format).
4a. Option 1: Force Password Reset (Recommended)
The safest method is to import users without passwords and send a password reset email:
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_KEY);
async function migrateUser(firebaseUser) {
// Create user in Supabase auth (password will be null)
const { data, error } = await supabase.auth.admin.createUser({
email: firebaseUser.email,
email_confirm: true, // if you already verified in Firebase
user_metadata: {
display_name: firebaseUser.displayName,
firebase_uid: firebaseUser.uid
}
});
if (error) {
console.error('Failed to create user:', error.message);
return;
}
// Send password reset email
await supabase.auth.admin.resetPasswordForEmail(firebaseUser.email);
console.log(User ${firebaseUser.email} migrated – password reset sent.);
}
This approach is honest and secure. Users will receive an email to set a new password on their first login.
4b. Option 2: Custom Claims Migration
If you have custom claims (e.g., “admin” roles), export them from Firebase and apply via Supabase’s updateUserById with app_metadata. Example:
// For each user with custom claims
await supabase.auth.admin.updateUserById(supabaseUserId, {
app_metadata: { role: 'admin' }
});
Supabase enforces RLS using claims from auth.jwt() – you can map Firebase roles directly.
Step 5: Migrate Storage Files
Uploading files from your local backup to Supabase Storage is straightforward using the Supabase JavaScript SDK.
5a. Create Buckets
In the Supabase dashboard, create buckets with the same names as your Firebase buckets (e.g., user-uploads, profile-pictures). Set the appropriate public/private access.
5b. Upload Files
const fs = require('fs');
const path = require('path');
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_KEY);
async function uploadFile(filePath, bucketName, remotePath) {
const fileBuffer = fs.readFileSync(filePath);
const { data, error } = await supabase.storage
.from(bucketName)
.upload(remotePath, fileBuffer, {
cacheControl: '3600',
contentType: 'application/octet-stream', // detect MIME type if needed
upsert: true
});
if (error) throw error;
console.log(Uploaded ${remotePath});
}
// iterate over your local files
const files = fs.readdirSync('./local-backup/user-uploads');
for (const file of files) {
await uploadFile(./local-backup/user-uploads/${file}, 'user-uploads', uploads/${file});
}
Note: Firestore stores file URLs that include a token (e.g.,?alt=media&token=abc). Supabase doesn’t use tokens by default – you’ll need to generate signed URLs if you require expiring access. Usesupabase.storage.from('bucket').createSignedUrl(path, expiresIn).
Troubleshooting Common Issues
Data type mismatches
- Firestore integers are 64‑bit, but PostgreSQL
INTEGERis 32‑bit. UseBIGINTfor user IDs or counters. - Firestore arrays become JSON arrays in SQL. Consider using a junction table if you need relational queries.
- GeoPoints don’t have a direct PostgreSQL equivalent. Use
pointtype orearthdistanceextension.
Nested objects
Flatten deeply nested maps like this:
-- Instead of storing { address: { city: "NYC", zip: "10001" } }
-- Create columns address_city and address_zip in your table.
ALTER TABLE profiles ADD COLUMN address_city TEXT;
ALTER TABLE profiles ADD COLUMN address_zip TEXT;
Authentication tokens
Firebase uses JWT with a specific structure. Supabase’s JWT includes aud, iss, and sub fields that differ. You’ll need to update your client‑side token verification logic (switch from verifyIdToken to supabase.auth.getUser()).
Downtime concerns
To minimise downtime, run your migration in a maintenance window:
- Disable Firestore writes from the client app.
- Run the data import.
- Redeploy your app backend pointing to Supabase.
- Re‑enable writes on the new backend.
If you need zero‑downtime, consider a dual‑write approach for a short period (write to both Firebase and Supabase), then switch readers.
Final Verdict
Migrating from Firebase to Supabase is a significant project – it took me about two days for a moderately complex app. The effort is absolutely worth it if:
- You want to escape vendor lock‑in and own your data.
- You need cost predictability (Firestore costs can spike without warning).
- You prefer SQL over NoSQL for complex queries.
| Feature | Firebase | Supabase |
|---|---|---|
| Pricing | Pay per read/write | Flat monthly + storage |
| Database | NoSQL (Firestore) | PostgreSQL |
| Realtime | Via Firestore listeners | Via database replication |
| Auth | Proprietary | Built‑in + external providers |
| Open source | No | Yes (AGPL v3) |
| Self-hostable | No | Yes |
For most indie hackers and small teams, Supabase is the clear winner. The only case to stay with Firebase is if you need heavy integration with Google Cloud (Cloud Functions, ML Kit) or you’re already deeply embedded in Firebase Analytics.
Ready to make the switch? Start by signing up for Supabase – the free tier gives you 500 MB database, 1 GB storage, and 50k monthly active users. If you later need to host your own Supabase instance, consider DigitalOcean.com) droplets (I use their $12/mo plan for staging).
Good luck, and don’t forget to test everything in a staging environment first!
🔍 Want the best deal? Check current prices and availability.
Compare Prices →