iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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' });

Test yourself

Q1. Firestore is…
Q2. A query on a missing index typically…
Q3. Listening for real-time updates uses…

Discussion

Loading…