Draft, not yet published
Migrating from Firebase to Postgres: a complete guide
7 July 2026· 5 min read · by Stackbastion
Firebase got your app off the ground fast. Now the bill climbs with every read, the queries you need aren’t possible, and you want real SQL. Moving from Firestore to Postgres is a real project, but it’s a well-worn path. Here’s how to do it without dropping data on the floor.
Why the move is more than a data copy
Firestore is a document database. It stores nested JSON-like documents in collections. Postgres is relational: rows, columns, tables, and foreign keys. You’re not just copying data, you’re reshaping it.
That reshaping is the hard part. A Firestore document can nest arrays and sub-objects three levels deep. In Postgres you decide: does that nested data become its own table, or a jsonb column? Get this wrong and you’ll fight your schema for months. Get it right and your queries get faster and cheaper.
Step 1: export your Firestore data
First, get everything out. Firebase’s managed export drops your data to a Cloud Storage bucket, but that format is meant for re-importing into Firestore, not for reading. For a migration you want readable JSON.
The cleaner path is a small script using the Admin SDK to walk every collection and write JSON files:
import admin from "firebase-admin";
admin.initializeApp({ credential: admin.credential.applicationDefault() });
const db = admin.firestore();
async function exportCollection(name) {
const snapshot = await db.collection(name).get();
const docs = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
await Bun.write(`export/${name}.json`, JSON.stringify(docs, null, 2));
console.log(`${name}: ${docs.length} docs`);
}
await exportCollection("users");
await exportCollection("orders");
Run this for every top-level collection. For sub-collections, walk each parent document and export its children with a reference back to the parent id. Keep those parent ids: they become your foreign keys.
Step 2: design the Postgres schema
Now map documents to tables. Go collection by collection.
- A flat collection (users with name, email, created date) maps cleanly to a table with one column per field.
- A reference between documents (an order that stores a
userId) becomes a foreign key. - A sub-collection (each user’s addresses) becomes its own table with a
user_idforeign key. - Deeply nested data that always travels together and you never query into (a settings blob) can live in a single
jsonbcolumn. Don’t overuse this. If you filter or sort by a field, give it a real column.
A rough example:
CREATE TABLE users (
id text PRIMARY KEY, -- keep the Firestore doc id
email text UNIQUE NOT NULL,
display_name text,
created_at timestamptz NOT NULL DEFAULT now(),
settings jsonb -- rarely-queried blob
);
CREATE TABLE orders (
id text PRIMARY KEY,
user_id text NOT NULL REFERENCES users(id),
total_cents integer NOT NULL,
status text NOT NULL,
created_at timestamptz NOT NULL
);
Keep the original Firestore document ids as your primary keys during the migration. It makes matching records across the two systems far easier while you’re verifying.
Step 3: transform and load
Write a loader that reads your exported JSON, reshapes each record to match the schema, and inserts it. Watch for the Firestore-specific traps:
- Timestamps. Firestore timestamps export as objects with seconds and nanoseconds. Convert them to real
timestamptzvalues. - Missing fields. Firestore documents in the same collection can have different fields. Postgres columns need a value or a null. Decide a default for each.
- References. A Firestore
DocumentReferenceexports as a path string. Turn it into the plain id your foreign key expects.
Load parents before children so foreign keys resolve. Wrap each collection’s load in a transaction so a failure rolls back cleanly instead of leaving half the data in.
Step 4: migrate auth
If you use Firebase Authentication, users can’t just be copied, because Firebase hashes passwords with its own scrypt setup. You have two workable options:
- Export the password hashes. The Firebase CLI can export users including their scrypt hash and parameters (
firebase auth:export). You can verify against those hashes in your new system if you reimplement Firebase’s scrypt variant. This is fiddly but keeps everyone’s password working. - Force a password reset. Import users without passwords and email everyone a reset link on cutover. Simpler and safer, but it’s friction for your users.
Most small apps take option 2. It’s honestly less risky than getting a custom hash-verify wrong.
Step 5: verify, then cut over
Before you switch anything live:
- Count rows per table and compare to Firestore document counts. They should match.
- Spot-check 20 records by id across both systems.
- Run your app’s real queries against the new Postgres and confirm the results.
For the cutover itself, the safest pattern is a short read-only window: freeze writes to Firestore, run a final delta export of anything changed since your last export, load it, then point your app at Postgres. For a small app that window can be a few minutes at a quiet hour.
Or, we do it for you
A Firebase-to-Postgres move is exactly the kind of one-time job that goes sideways when you do it live and alone. Our Rescue service handles the export, schema mapping, auth migration, and a verified cutover, so you don’t lose data or a weekend. If you’d rather DIY the database itself, the Supabase to Postgres guide covers a related path.
FAQ
How long does a Firebase to Postgres migration take?
For a small app with a handful of collections, a careful person can do it in a few days: a day to design the schema, a day to write and test the loaders, and a short cutover. Bigger apps with sub-collections, auth, and Cloud Functions take longer because there’s more to reshape and re-test.
Will my app be down during the migration?
It doesn’t have to be down for long. You do the bulk export and load while the app runs normally on Firebase. Only the final cutover needs a short read-only window, often just a few minutes, to sync the last changes and switch over.
Can I keep some data in Firestore and move the rest?
You can, but running two databases is more work, not less. It’s usually cleaner to move everything in one project. If you must split, move the data that needs SQL queries and relationships first, since that’s where Firestore hurts most.
What happens to my Firebase Cloud Functions?
They don’t migrate automatically. Cloud Functions are code, so you port them to whatever runs your Postgres app: a Node or Bun service, scheduled jobs, or database triggers. Plan for this. It’s often the most overlooked part of leaving Firebase.