
Why We're Never Going Back to Firebase
Firebase is great — until it isn't. We used it on multiple client projects over the years. Real-time sync out of the box, no server to manage, generous free tier. For prototypes and hackathons, it's hard to beat.
But every single project that grew beyond the prototype phase hit the same walls. After the third painful migration, we made a decision: PostgreSQL is our default. No exceptions.
The Honeymoon Phase
Let's be fair. Firebase gets a lot right for early-stage products:
- Zero backend setup — just
npm install firebaseand go - Real-time listeners with
onSnapshotfeel like magic - Authentication in minutes, not days
- Hosting, storage, and functions in one ecosystem
We genuinely enjoyed building with it. The first few weeks of any Firebase project feel incredibly productive.
Where It Falls Apart
1. NoSQL = NoSafety
Firestore has no schema. That sounds like freedom. In practice, it's chaos.
// Firestore: hope for the best
const userDoc = await getDoc(doc(db, "users", id));
const data = userDoc.data(); // type: DocumentData | undefined
// What fields exist? Who knows. Check the console.
// PostgreSQL + Prisma: know exactly what you're getting
const user = await prisma.user.findUnique({
where: { id },
select: { name: true, email: true, role: true },
});
// user is fully typed. IDE autocomplete works. Typos are compile errors.
A typo in a Firestore field name doesn't throw an error — it silently creates a new field. We've spent entire debugging sessions tracking down issues that turned out to be userName vs username vs user_name scattered across a codebase.
A client's production database had 14 different spellings of address-related fields across 50,000 documents. There was no way to catch this without manually inspecting documents. With a SQL schema, this is literally impossible.
2. TypeScript Ends at the Network Boundary
This is the killer. You can have the most beautifully typed TypeScript frontend in the world, and Firestore will still hand you any at the database layer.
Yes, you can use converters. Yes, you can write Zod schemas. But you're maintaining types twice — once in your code and once in your head, hoping they match what's actually in the database.
With Prisma, the types are generated from the schema. One source of truth. Change the schema, run prisma generate, and TypeScript tells you everywhere your code needs to update.
// prisma/schema.prisma — this IS the truth
model Order {
id String @id @default(cuid())
status OrderStatus
total Decimal
items OrderItem[]
createdAt DateTime @default(now())
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
}
Try doing that with Firestore. You can't enforce an enum at the database level. You can't guarantee a field exists. You can't ensure referential integrity.
3. Security Rules Are Their Own Language
Firestore security rules are a custom DSL with no IDE support, no unit testing framework (yes, there's the emulator, but it's painful), and error messages that read like riddles.
// Firestore rules — good luck debugging this at scale
match /orders/{orderId} {
allow read: if request.auth != null
&& resource.data.userId == request.auth.uid;
allow write: if request.auth != null
&& request.resource.data.keys().hasAll(['status', 'total'])
&& request.resource.data.status in ['pending', 'confirmed'];
}
Compare that to middleware in Next.js or row-level security in PostgreSQL — tools with actual type checking, testing frameworks, and years of ecosystem support.
4. Cost Surprises
Firestore charges per document read. Sounds simple until you realize:
- A list view showing 50 items = 50 reads
- A real-time listener on that list = 50 reads every time anything changes
- Paginating through results still costs reads for skipped documents
- There's no
SELECT COUNT(*)— you either maintain a counter or read every document
We had a client whose monthly bill jumped from $30 to $800 because a single dashboard component was triggering cascading reads. With PostgreSQL, you pay for compute, not queries.
5. Vendor Lock-In Is Real
Every Firestore query uses proprietary syntax. Every security rule is platform-specific. Every Cloud Function is tied to the Firebase ecosystem.
SQL has been around for 50 years. Your queries work on PostgreSQL, MySQL, SQLite, CockroachDB, PlanetScale, Neon, Supabase, and a hundred other databases. Your knowledge transfers. Your code is portable.
We've migrated three projects from Firebase to PostgreSQL. Each time, it meant rewriting every single data access layer. With SQL, switching from one provider to another is a connection string change.
What We Use Instead
Our current default stack:
| Layer | Tool | Why |
|---|---|---|
| Database | PostgreSQL | Battle-tested, typed, relational |
| ORM | Prisma | Generated types, migrations as code |
| Auth | better-auth | Framework-agnostic, modern, open-source |
| Real-time, Caching & Revalidation | Polling + SWR | Simple, predictable, no complexity |
| Hosting | Vercel / Hetzner | Predictable pricing |
When Firebase Still Makes Sense
We're not dogmatic. Firebase is a good choice when:
- You're building a real-time consumer app with simple, flat data
- Your team is small and doesn't have backend expertise
- You need to ship a prototype in days, not weeks
- You don't care about long-term maintainability (hackathons, experiments)
But for anything that needs to grow, evolve, and be maintained by a team? SQL wins. Every time.
The Bottom Line
We don't hate Firebase. We hate debugging production issues at 2 AM because a document had staus instead of status and nobody noticed for three weeks.
We love sleeping at night. And PostgreSQL lets us do that.
Considering a migration from Firebase? We handle infrastructure transitions with zero downtime. See our Cloud & DevOps services.

