Cloud Firestore
Firestore is Firebase’s document database — collections of documents, real-time sync, offline support, security rules at the row level. The default backing store for most Firebase apps.
Read, write, real-time, rules, batches
EXAMPLE
import { initializeApp } from 'firebase/app';
import {
getFirestore,
collection, doc, query, where, orderBy, limit,
getDoc, getDocs, addDoc, setDoc, updateDoc, deleteDoc,
onSnapshot, serverTimestamp,
writeBatch, runTransaction,
} from 'firebase/firestore';
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
// 1) Read a single document
const snap = await getDoc(doc(db, 'users', 'u_42'));
if (snap.exists()) {
console.log(snap.data()); // { name: 'Ada', email: '...' }
}
// 2) Read a collection
const snap2 = await getDocs(collection(db, 'posts'));
const posts = snap2.docs.map(d => ({ id: d.id, ...d.data() }));
// 3) Filtered + ordered query
const q = query(
collection(db, 'posts'),
where('status', '==', 'published'),
where('category', '==', 'tech'),
orderBy('createdAt', 'desc'),
limit(20),
);
const feed = (await getDocs(q)).docs.map(d => ({ id: d.id, ...d.data() }));
// 4) Write — add (auto-generated ID)
const ref = await addDoc(collection(db, 'posts'), {
title: 'Hello world',
body: '…',
authorId: currentUid,
createdAt: serverTimestamp(),
status: 'published',
});
console.log(ref.id); // auto-generated ID
// Write — set (specify ID)
await setDoc(doc(db, 'users', 'u_42'), {
name: 'Ada',
email: 'ada@example.com',
createdAt: serverTimestamp(),
});
// Set with merge — patch existing fields, keep others
await setDoc(doc(db, 'users', 'u_42'), { lastSeen: serverTimestamp() }, { merge: true });
// Update (errors if doc doesn't exist)
await updateDoc(doc(db, 'users', 'u_42'), {
'profile.bio': 'Engineer',
lastSeen: serverTimestamp(),
});
// Field operations
import { increment, arrayUnion, arrayRemove, deleteField } from 'firebase/firestore';
await updateDoc(doc(db, 'posts', 'p_1'), {
views: increment(1),
tags: arrayUnion('featured', 'editor-pick'),
legacyField: deleteField(),
});
// Delete
await deleteDoc(doc(db, 'posts', 'p_1'));
// 5) Real-time listeners — onSnapshot
const unsub = onSnapshot(doc(db, 'users', 'u_42'), (snap) => {
setUser(snap.data());
});
// Later:
unsub();
// Collection listener
const unsub2 = onSnapshot(
query(collection(db, 'chats', chatId, 'messages'), orderBy('ts'), limit(50)),
(snap) => {
snap.docChanges().forEach(change => {
if (change.type === 'added') addMessage(change.doc.data());
if (change.type === 'modified') updateMessage(change.doc.data());
if (change.type === 'removed') removeMessage(change.doc.id);
});
},
);
// 6) Pagination (cursor-based)
import { startAfter } from 'firebase/firestore';
const first = await getDocs(query(
collection(db, 'posts'),
orderBy('createdAt', 'desc'),
limit(20),
));
const lastVisible = first.docs[first.docs.length - 1];
const second = await getDocs(query(
collection(db, 'posts'),
orderBy('createdAt', 'desc'),
startAfter(lastVisible),
limit(20),
));
// 7) Subcollections
const messagesRef = collection(db, 'chats', chatId, 'messages');
await addDoc(messagesRef, { text: 'hi', userId: currentUid, ts: serverTimestamp() });
// 8) Batch writes — up to 500 ops atomically
const batch = writeBatch(db);
batch.set(doc(db, 'users', 'u_1'), { name: 'Ada' });
batch.update(doc(db, 'posts', 'p_1'), { views: increment(1) });
batch.delete(doc(db, 'posts', 'p_2'));
await batch.commit();
// 9) Transactions — read + write atomically
await runTransaction(db, async (tx) => {
const ref = doc(db, 'inventory', 'sku_A100');
const snap = await tx.get(ref);
if (!snap.exists()) throw new Error('not found');
const stock = snap.data().stock;
if (stock <= 0) throw new Error('out of stock');
tx.update(ref, { stock: stock - 1 });
tx.set(doc(db, 'orders', orderId), { sku: 'A100', userId: currentUid });
});
// 10) Aggregations — count, sum, average (server-side, cheap)
import { getCountFromServer, getAggregateFromServer, sum, average, aggregateField } from 'firebase/firestore';
const c = await getCountFromServer(query(collection(db, 'orders'), where('status', '==', 'paid')));
console.log(c.data().count);
const stats = await getAggregateFromServer(query(collection(db, 'orders')), {
totalRevenue: sum('total'),
avgOrder: average('total'),
count: aggregateField('count'),
});
console.log(stats.data());
// 11) Security rules (firestore.rules)
rules_version = '2';
service cloud.firestore {
match /databases/{db}/documents {
// Users can read/write only their own profile
match /users/{uid} {
allow read: if request.auth != null && request.auth.uid == uid;
allow write: if request.auth != null && request.auth.uid == uid;
}
// Posts — anyone can read published; only author can write
match /posts/{postId} {
allow read: if resource.data.status == 'published'
|| (request.auth != null && request.auth.uid == resource.data.authorId);
allow create: if request.auth != null
&& request.resource.data.authorId == request.auth.uid;
allow update: if request.auth != null
&& request.auth.uid == resource.data.authorId
&& request.resource.data.authorId == resource.data.authorId;
allow delete: if request.auth != null && request.auth.uid == resource.data.authorId;
}
// Admin can do anything
match /{document=**} {
allow read, write: if request.auth.token.role == 'admin';
}
}
}
// 12) Composite indexes — Firestore auto-prompts when needed
// In the console, you'll see a link to create the index when a multi-condition
// query first runs. Or define in firestore.indexes.json:
// {
// "indexes": [
// {
// "collectionGroup": "posts",
// "queryScope": "COLLECTION",
// "fields": [
// { "fieldPath": "status", "order": "ASCENDING" },
// { "fieldPath": "category", "order": "ASCENDING" },
// { "fieldPath": "createdAt", "order": "DESCENDING" }
// ]
// }
// ]
// }
// 13) Offline support — built in for web (IndexedDB) and mobile (SQLite)
import { enableIndexedDbPersistence } from 'firebase/firestore';
await enableIndexedDbPersistence(db);
// Reads/writes work offline; sync when reconnected.
// 14) Data modelling tips
// • Denormalise — Firestore has no JOINs; duplicate data into the read shape
// • Keep documents < 1MB; flat hierarchies are faster than deep nesting
// • Don't store huge arrays — split into a subcollection
// • Use subcollections for unbounded child sets (messages in a chat)
// • Use field paths in updates ('a.b.c') for nested map updates
// 15) Cost levers
// • Each document read costs money — paginate everything
// • onSnapshot listens count as reads on changes — unsubscribe when not visible
// • Aggregate queries (count, sum) cost much less than reading all docs
// • Denormalise to reduce read counts (e.g. cache user.name on each post)
// 16) Common bugs
// • Forgetting unsub from onSnapshot → memory leak + cost
// • Using onSnapshot for one-off reads → unnecessary listener cost
// • Reading whole collection without limit() → kills your bill on a big collection
// • Wrong security rules → write goes through with no auth (or worse, can't write at all)
// • Not handling the offline-first model — local writes may take seconds to sync
// 17) When to use Firestore vs alternatives
// Firestore : real-time, offline, mobile-first, auth integration, simple model
// PostgreSQL : relational, complex joins, strong consistency, less per-read cost
// MongoDB Atlas : document DB with aggregations, no real-time-with-offline magic
// Supabase / Hasura : Postgres + real-time, open-source alternatives
// 18) Best practices
// • Set up security rules BEFORE your app ships
// • Build aggregations as separate counter docs (e.g. /counters/posts) for cheap reads
// • Use TypeScript with converters for type safety
// • Test rules with the Emulator + firebase-functions-test
// • Use App Check to reject non-app traffic
Why it matters
Firestore is real-time + offline-first out of the box. Security rules are non-negotiable — ship them with your first deploy. Denormalise aggressively (Firestore has no JOINs) and aggregate via counter documents to keep read costs low.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { getFirestore, collection, addDoc, getDocs } from 'firebase/firestore';
const db = getFirestore(app);
await addDoc(collection(db, 'users'), { name: 'Ada', age: 36 });
const snap = await getDocs(collection(db, 'users'));
snap.forEach(d => console.log(d.id, d.data()));
Try it Yourself »
Exercise
Add a new document.
await
(collection(db, 'users'), { name: 'Ada' });
Six letters.
Discussion
Loading…