Engineering

Leaving Firebase for Postgres

Firestore is the fastest database I know of to get a first version writing data. You open a collection, push a document, and nothing stops you. The bill for that arrives later, in the shape of a query you cannot write and an index you cannot avoid paying for. Google's own docs are honest about both, so this post mostly just reads them.

The query language has a hard edge

Two rules in the Firestore documentation catch most teams off guard. The first: when you order a query by a field, it returns only the documents where that field exists. A filter like greaterThan on population silently implies an order by population, so every record missing that field disappears from the result. Nothing errors. The row count is just quietly wrong, and you find out from a customer.

The second is the OR limit. Firestore converts a query to disjunctive normal form before running it, and the result cannot exceed 30 disjunctions. An innocent looking pair of filters, one field matching any of five values and another matching any of ten, expands to 50 and gets rejected outright. In Postgres that is a WHERE clause with two IN lists and a composite index.

So you denormalize. You copy the customer name onto the order, the order total onto the customer, the status flag onto both. Every one of those copies is a field you now have to keep in sync in application code, because there is no constraint layer to do it for you. That work is real and it accumulates, which is the part nobody estimates.

The bill follows your schema, not your traffic

Firestore's free quota is genuinely generous: 1 GiB stored, 50,000 document reads a day, 20,000 writes, 20,000 deletes, and 10 GiB of monthly egress. Past that, in us-central1, reads cost $0.03 per 100,000 documents and writes cost $0.09 per 100,000. A write is three times the price of a read, checked on Google's pricing page today.

Now put that next to the denormalization the query limits pushed you into. One logical update becomes four document writes, and your cost per user action triples while your read pattern stays identical. There is a second charge underneath it. Google bills one read operation for every batch of up to 1,000 index entries a query scans, and queries with more than one range field are charged for that scan. A field in your order by clause counts as a range field the moment any other range filter exists.

None of this is hidden or unfair. It is priced exactly as documented. The problem is that the number you are optimizing has almost no relationship to how many people used your product, which makes it very hard to reason about before you ship.

What the migration is actually made of

The data move is the boring part, and it is the part people fear most. When we took Stay World Class off Webflow and Xano onto Next.js, NestJS and Supabase, the whole transfer was a four stage ETL whose only clever idea was keeping a mapping from every legacy identifier to its new PostgreSQL UUID. Firestore needs the same trick: document IDs are arbitrary strings of up to 1,500 bytes, so you carry a legacy_id column, backfill relationships against it, and drop it when nothing references it any more.

The genuinely hard part is the logic that was living in security rules. Firestore caps a ruleset at 10 get() calls per single document request and 1,000 evaluated expressions, which means your authorization is written in a small language you cannot unit test outside the platform. Moving it to row level security in Postgres, or to a service layer with real tests, is where most of the calendar time goes.

What you buy is ordinary. Joins. Transactions across tables. A migration file in version control that a reviewer can read. On the Stay World Class rebuild the visible result was Lighthouse going from 55.91 to 91 out of 100 and largest contentful paint dropping 77 percent, though that came from the whole stack rather than the database alone.

When you should stay on Firestore

If your access pattern is key lookups and live listeners, Firestore is very good at exactly that, and a naive Postgres port would be a downgrade on the realtime side. Chat, presence, collaborative cursors, anything where clients need a push rather than a poll. Rebuilding that on top of Postgres is a project in itself, and you should want a better reason than tidiness.

Firebase also gives you a middle path now. SQL Connect runs Cloud SQL for PostgreSQL inside the Firebase project, with a three month no cost trial for the first instance and pricing from about $9.37 a month afterward, so the relational parts of your app can be relational without moving anything else. That is a smaller decision than a migration and it reverses cleanly.

And if you are still inside the free quota, the honest answer is to do nothing. Fifty thousand reads a day is a real business for a lot of products. Migrate when the query you need is the one Firestore will not run, or when your write amplification is showing up on a bill you can point at. Wanting SQL is not a reason on its own.

Sources

Is your platform outgrowing its stack?

Book a call and we'll walk through how we'd approach your platform, with an estimate grounded in a working prototype.