REF / WRITING · SOFTWARE

Firebase for Modern App Developers: The Complete 2026 Guide

The complete Firebase guide - Firestore, Auth, Realtime Database, Storage, Cloud Functions, and when Firebase is (and isn't) the right choice.

DomainSoftware
Formattutorial
Published9 Jul 2024
Tagsfirebase · firestore · google

Firebase is Google's application development platform. a fully managed suite of backend services designed for mobile and web apps. At its core: Firestore (a NoSQL document database), Authentication, Realtime Database, Cloud Storage, Cloud Functions, and Hosting, all integrated under one SDK and billed through a single Google Cloud account.

Millions of apps run on Firebase. It has the broadest mobile SDK support of any BaaS (iOS, Android, Flutter, React Native, web), direct integration with every Google service, and a free tier (Spark plan) generous enough to take most side projects to launch.

This guide is the complete reference: Firestore data modeling, queries, security rules, authentication flows, Cloud Functions, and a clear-eyed assessment of when Firebase is and isn't the right choice.

The Firebase Stack: What You Get

ServiceWhat It Does
FirestoreScalable NoSQL document database, realtime sync
Realtime DatabaseOlder JSON tree database, lower latency
AuthenticationEmail, social OAuth, phone, anonymous
Cloud StorageFile storage backed by Google Cloud Storage
Cloud FunctionsServer-side TypeScript/JavaScript functions
HostingCDN-backed static hosting
App CheckAttestation. only your app can call your Firebase
AnalyticsGoogle Analytics integrated
CrashlyticsCrash reporting for mobile

For new projects: use Firestore (not Realtime Database). Firestore has richer queries, better scalability, and is the clearly preferred option going forward.

Setup

npm install firebase

Initialize in your app:

// lib/firebase.ts
import { initializeApp, getApps } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";
import { getStorage } from "firebase/storage";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

const app = getApps().length ? getApps()[0] : initializeApp(firebaseConfig);

export const db = getFirestore(app);
export const auth = getAuth(app);
export const storage = getStorage(app);

The getApps().length check prevents re-initialization in Next.js hot reloads.

Firestore Data Modeling

Firestore is a document database. data is stored as JSON documents inside collections. The key skill is data modeling: how you structure your documents determines what queries are possible.

Collections and documents

/users/{userId}             - user profile documents
/users/{userId}/posts/{postId}  - subcollection: user's posts
/posts/{postId}             - top-level collection: all posts
/organizations/{orgId}      - org documents

Denormalization is normal

Unlike relational databases, Firestore is designed for denormalization. Duplicate data to avoid expensive joins:

// Document: /posts/{postId}
{
  id: "post_abc",
  title: "My Post Title",
  content: "...",
  authorId: "user_xyz",
  // Denormalized: author data duplicated here
  authorName: "Jane Smith",
  authorAvatarUrl: "https://...",
  publishedAt: Timestamp,
  tagSlugs: ["react", "typescript"],
  commentCount: 42,  // Denormalized counter
}

Why? Fetching a list of posts with author names requires either a separate query per post (N+1) or embedding author data. Firestore has no JOINs; denormalization is the answer.

Trade-off: When the author changes their name, you need to update all their posts. Use Cloud Functions triggered on user profile updates to propagate changes.

CRUD Operations

import { 
  collection, doc, addDoc, setDoc, getDoc, getDocs,
  updateDoc, deleteDoc, query, where, orderBy, limit,
  onSnapshot, serverTimestamp, increment
} from "firebase/firestore";
import { db } from "@/lib/firebase";

// Create
const postRef = await addDoc(collection(db, "posts"), {
  title: "New Post",
  content: "...",
  authorId: userId,
  publishedAt: serverTimestamp(),
});

// Read one
const snap = await getDoc(doc(db, "posts", postId));
const post = snap.exists() ? { id: snap.id, ...snap.data() } : null;

// Read many with query
const q = query(
  collection(db, "posts"),
  where("authorId", "==", userId),
  orderBy("publishedAt", "desc"),
  limit(20)
);
const snaps = await getDocs(q);
const posts = snaps.docs.map(d => ({ id: d.id, ...d.data() }));

// Update (merge)
await updateDoc(doc(db, "posts", postId), {
  title: "Updated Title",
  updatedAt: serverTimestamp(),
});

// Increment a counter atomically
await updateDoc(doc(db, "posts", postId), {
  commentCount: increment(1),
});

// Delete
await deleteDoc(doc(db, "posts", postId));

Realtime Listeners

Firestore's realtime capability is its strongest feature for collaborative apps:

// Listen to a single document
const unsubscribe = onSnapshot(doc(db, "posts", postId), (snap) => {
  if (snap.exists()) {
    setPost({ id: snap.id, ...snap.data() });
  }
});

// Listen to a collection query
const q = query(
  collection(db, "posts"),
  where("published", "==", true),
  orderBy("publishedAt", "desc"),
  limit(10)
);

const unsubscribe = onSnapshot(q, (snapshot) => {
  const posts = snapshot.docs.map(d => ({ id: d.id, ...d.data() }));
  setPosts(posts);
});

// Always unsubscribe when the component unmounts
useEffect(() => () => unsubscribe(), []);

Authentication

import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signInWithPopup,
  GoogleAuthProvider,
  signOut,
  onAuthStateChanged,
} from "firebase/auth";
import { auth } from "@/lib/firebase";

// Email sign-up
await createUserWithEmailAndPassword(auth, email, password);

// Email sign-in
await signInWithEmailAndPassword(auth, email, password);

// Google OAuth
const provider = new GoogleAuthProvider();
const result = await signInWithPopup(auth, provider);
const user = result.user;

// Sign out
await signOut(auth);

// Listen to auth state (put this in your app root)
onAuthStateChanged(auth, (user) => {
  if (user) {
    setCurrentUser(user);
  } else {
    setCurrentUser(null);
  }
});

Creating user profiles in Firestore

// After sign-up, create a profile document
await setDoc(doc(db, "users", user.uid), {
  uid: user.uid,
  email: user.email,
  displayName: user.displayName ?? "",
  photoURL: user.photoURL ?? "",
  createdAt: serverTimestamp(),
});

Firestore Security Rules

Security rules determine who can read/write what. They're enforced server-side. client SDK calls that violate rules are rejected.

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

    // Helper functions
    function isAuthenticated() {
      return request.auth != null;
    }

    function isOwner(userId) {
      return request.auth.uid == userId;
    }

    // Users can read/write their own profile
    match /users/{userId} {
      allow read: if isAuthenticated();
      allow write: if isOwner(userId);
    }

    // Posts: public read, authenticated create, owner write
    match /posts/{postId} {
      allow read: if resource.data.published == true
                  || (isAuthenticated() && isOwner(resource.data.authorId));
      allow create: if isAuthenticated()
                    && request.resource.data.authorId == request.auth.uid;
      allow update, delete: if isOwner(resource.data.authorId);
    }

    // Comments: authenticated read/create, owner delete
    match /posts/{postId}/comments/{commentId} {
      allow read: if isAuthenticated();
      allow create: if isAuthenticated()
                    && request.resource.data.authorId == request.auth.uid;
      allow delete: if isOwner(resource.data.authorId);
    }
  }
}

Test rules with the Firebase Emulator before deploying:

firebase emulators:start

Cloud Functions

For server-side logic (sending emails, processing webhooks, scheduled jobs):

// functions/src/index.ts
import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { onCall } from "firebase-functions/v2/https";
import { onSchedule } from "firebase-functions/v2/scheduler";
import * as admin from "firebase-admin";

admin.initializeApp();

// Trigger on new document
export const onNewPost = onDocumentCreated("posts/{postId}", async (event) => {
  const post = event.data?.data();
  if (!post?.authorId) return;

  // Send notification to followers, update search index, etc.
  await admin.firestore()
    .collection("users").doc(post.authorId)
    .update({ postCount: admin.firestore.FieldValue.increment(1) });
});

// Callable function (called from client SDK)
export const sendWelcomeEmail = onCall(async (request) => {
  const { userId } = request.data;
  const user = await admin.auth().getUser(userId);
  // Send email via SendGrid/Resend
  return { success: true };
});

// Scheduled function
export const dailyCleanup = onSchedule("0 2 * * *", async () => {
  // Clean up old data, send digests, etc.
});

Deploy: firebase deploy --only functions

Common Pitfalls

1. Missing security rules: new Firestore projects default to open rules. Lock them down before launch.

2. Not using composite indexes: Firestore requires composite indexes for queries with multiple where clauses or where + orderBy on different fields. The error message includes a link to create the index automatically.

3. Unbounded collections: a document can have at most 1MB. Subcollections (e.g., /users/{id}/messages) are the right pattern for unbounded related data.

4. Deep nesting: Firestore supports subcollections up to 100 levels deep, but security rules become complex. Keep nesting shallow (1-2 levels).

5. Reading entire collections: always use queries with limits. getDocs(collection(db, "posts")) without a limit on a large collection will read every document and cost accordingly.

Firebase vs Supabase: When to Choose Which

ScenarioBetter Choice
Mobile-first (iOS/Android/Flutter)Firebase. SDKs are deeper and more mature
Relational data, complex queriesSupabase (Postgres)
SQL teamSupabase
Google ecosystemFirebase
Self-hosting requirementSupabase
Real-time collaborative featuresBoth are good; Firebase is simpler
Vendor lock-in concernSupabase

FAQ

Q: Is Firestore good for complex queries? Firestore supports basic filtering and sorting but no JOINs, no aggregations beyond count(), and no full-text search. For complex queries, complement with Algolia, Typesense, or BigQuery.

Q: How is Firestore pricing? Pay per document read/write/delete. Reads: $0.06/100k. Writes: $0.18/100k. On Spark (free): 50k reads/day, 20k writes/day. Budget carefully. high-frequency listeners can read many documents.

Q: Can I use Firebase with Next.js? Yes. Use the Firebase client SDK for browser components; the Admin SDK (firebase-admin) in Route Handlers and Server Components. See Firebase's Next.js guide.

Q: How do I handle offline support? Firestore has built-in offline persistence. Enable it with enableIndexedDbPersistence(db). The SDK queues writes locally and syncs when connectivity returns.

Q: What about Firebase vs AWS Amplify? Firebase is simpler and faster to start. Amplify gives you access to the full AWS ecosystem but has more configuration overhead.

Conclusion

Firebase remains a strong choice for mobile-first apps, rapid prototyping, and teams that need broad SDK support and deep Google Cloud integration. Firestore's realtime sync and Firebase Auth's OAuth support cover the core needs of most apps.

Know its limits: Firestore isn't the right database for complex relational data. If you need SQL, JOINs, or want to avoid vendor lock-in, Supabase is the better fit.

See also: Supabase: The Complete Developer Guide. the open-source Postgres alternative.