Back to Blog
Why We're Never Going Back to Firebase

Why We're Never Going Back to Firebase

Dennis Reinkober3 min read

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 firebase and go
  • Real-time listeners with onSnapshot feel 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.

Real Incident

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.

The Migration Tax

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:

LayerToolWhy
DatabasePostgreSQLBattle-tested, typed, relational
ORMPrismaGenerated types, migrations as code
Authbetter-authFramework-agnostic, modern, open-source
Real-time, Caching & RevalidationPolling + SWRSimple, predictable, no complexity
HostingVercel / HetznerPredictable 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.

Similar Posts