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
| Service | What It Does |
|---|---|
| Firestore | Scalable NoSQL document database, realtime sync |
| Realtime Database | Older JSON tree database, lower latency |
| Authentication | Email, social OAuth, phone, anonymous |
| Cloud Storage | File storage backed by Google Cloud Storage |
| Cloud Functions | Server-side TypeScript/JavaScript functions |
| Hosting | CDN-backed static hosting |
| App Check | Attestation. only your app can call your Firebase |
| Analytics | Google Analytics integrated |
| Crashlytics | Crash 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
| Scenario | Better Choice |
|---|---|
| Mobile-first (iOS/Android/Flutter) | Firebase. SDKs are deeper and more mature |
| Relational data, complex queries | Supabase (Postgres) |
| SQL team | Supabase |
| Google ecosystem | Firebase |
| Self-hosting requirement | Supabase |
| Real-time collaborative features | Both are good; Firebase is simpler |
| Vendor lock-in concern | Supabase |
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.