Article 29 – Firebase and Firestore

When you hear “Firebase,” you might think of hackathons and startup prototypes. That view is dangerously incomplete. Firebase is not a “lite” tool — it is a fully managed, opinionated suite of services running on Google Cloud’s global infrastructure. For the right architecture, it is a strategic advantage.

Let me give you the architect’s perspective on where Firebase and Firestore fit, what trade-offs you are making, and how to design systems that are secure and cost-effective.

Firebase Is Not Just a Database

First, let us clear up the confusion. Firestore is a database. Firebase is the platform — a comprehensive Backend-as-a-Service (BaaS) that includes:

  • Databases — Cloud Firestore and Realtime Database
  • Compute — Cloud Functions for Firebase
  • Identity — Firebase Authentication (user sign-in management)
  • Storage — Cloud Storage for Firebase (user-generated files)
  • Hosting — Firebase Hosting (global CDN for static assets)
  • Ops — Crashlytics, Performance Monitoring, Google Analytics

When you adopt Firebase, you are buying into an entire managed, event-driven ecosystem. The real power comes from combining: Firestore triggers a Cloud Function that processes an image saved to Cloud Storage, all authenticated through Firebase Auth.

Firestore vs. Realtime Database

Firebase has two NoSQL databases. Choosing the right one matters:

FeatureCloud FirestoreRealtime Database
Data ModelDocument-Collection (hierarchical)Single large JSON tree
QueryingRich, indexed, compound queriesPath-based, limited filtering
ScalabilityMassive, automatic, multi-regionalScales by manual sharding
Offline SupportExcellent (Mobile/Web)Good (Mobile)
PricingPer operation (read/write) + storagePer storage + bandwidth

For 90% of new applications, Cloud Firestore is the default. It is designed for global scale, offers robust querying, and has a more structured data model.

The Realtime Database is a specialized tool for extremely high-frequency state-syncing — think collaborative editors or real-time games with very simple data structures.

Data Modeling — Stop Thinking Like SQL

This is the most critical thing. If you come from a relational background, you must shift your thinking. Treating Firestore like a SQL database is the number one cause of performance problems and cost blowouts.

Firestore stores documents (JSON-like objects) in collections. Documents can point to subcollections:

users (collection)
  └── user_alice (document)
      ├── name: "Alice"
      ├── email: "[email protected]"
      └── orders (subcollection)
          └── order_123 (document)
              ├── item: "SKU-456"
              └── amount: 99.99
The Golden Rule: Design for Your Queries

In SQL, you design for data integrity (normalization). In Firestore, you design for your application’s access patterns.

This means you denormalize — duplicate data to make reads fast.

SQL approach (bad for Firestore):

/users/alice
/posts/post_abc (author: "alice")
/posts/post_xyz (author: "alice")

Query: Get user “alice.” Then query “posts” where author == “alice.” Two round trips.

Firestore approach (good):

/users/alice
  ├── name: "Alice"
  └── recent_posts: [
        { id: "post_xyz", title: "My new post" },
        { id: "post_abc", title: "Hello world" }
      ]

Query: Get user “alice.” All data is there in one read.

Yes, data is duplicated. When Alice updates her name, you need to update it in her user doc AND every post. That is the trade-off: write-time complexity for massive read-time performance.

How do you manage the duplication? Cloud Functions, which react to data changes and propagate updates.

Security Rules — Your Primary Security Model

In a serverless Firebase architecture, the client connects directly to the database. No app server in between. This sounds terrifying, until you understand Security Rules.

Security Rules are evaluated on every single read, write, and delete. They are your firewall, validator, and auth controller combined.

The #1 mistake — the test mode trap:

// DANGEROUS! DO NOT USE IN PRODUCTION!
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;  // Your entire database is exposed
    }
  }
}

A production-ready ruleset:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Users can only read/write their own profile
    match /users/{userId} {
      allow read, update, delete: if request.auth.uid == userId;
      allow create: if request.auth.uid != null;
    }

    // Anyone can read posts, only the author can write
    match /posts/{postId} {
      allow read: if true;
      allow create, update, delete: if request.auth.uid == resource.data.author_id;
    }
  }
}
Cloud Functions — The Integration Layer

How do you handle denormalization, send emails, process payments? You cannot do that on the client. Cloud Functions for Firebase are event-driven, serverless functions that react to triggers in your Firebase ecosystem.

Here is a function that solves our denormalization problem. When a user updates their name, it finds all their posts and updates the author name:

// index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();

exports.updateAuthorName = functions.firestore
    .document('users/{userId}')
    .onUpdate(async (change, context) => {
      const newValue = change.after.data();
      const previousValue = change.before.data();
      const userId = context.params.userId;

      // If name didn't change, do nothing
      if (newValue.name == previousValue.name) return null;

      console.log(`User ${userId} name changed. Fanning out...`);

      // Find all posts by this user
      const postsRef = db.collection('posts').where('author_id', '==', userId);
      const snapshot = await postsRef.get();

      // Batch update all of them
      const batch = db.batch();
      snapshot.forEach(doc => {
        batch.update(doc.ref, { author_name: newValue.name });
      });

      return batch.commit();
    });
The Cost Model — Design For It

Firebase billing is based on operations, not compute:

  • Number of reads
  • Number of writes
  • Number of deletes
  • Data storage and network egress

A poorly designed query can cost thousands, even if it returns one document.

The N+1 Query Pitfall:

ApproachMethodCost
BadFetch 100 friend IDs, then loop and fetch each document101 reads
BetterUse in operator to batch-fetch~10 reads
BestDenormalize friend names onto the user document1 read

Performance and cost are two sides of the same coin in Firestore.

When to Use Firebase

Use it when:
– Your project is client-heavy (web, mobile)
– You need real-time data sync
– You want to minimize server management
– Your access patterns are well-defined
– You are building an event-driven, serverless architecture

Be cautious when:
– You need complex relational queries and ad-hoc analytics (use SQL instead)
– Your workload is write-heavy, read-light (cost model may be unfavorable)
– Your team is not willing to learn NoSQL data modeling

Quick Reference
# Initialize a Firebase project
firebase init

# Deploy everything (rules, functions, hosting)
firebase deploy

# Deploy only security rules
firebase deploy --only firestore:rules

# Deploy only Cloud Functions
firebase deploy --only functions

# Start local emulator suite (critical for testing)
firebase emulators:start

# List all Firebase projects
firebase projects:list

The Firebase Emulator Suite is non-negotiable for professional teams. It runs a local version of Firestore, Auth, and Functions so you can test security rules and triggers without cloud costs.