Cloud Firestore Guide: Document Model for Scalable NoSQL Data
In this tutorial, you will learn about Cloud Firestore Guide: Document Model for Scalable NoSQL Data. We cover key concepts, practical examples, and best practices to help you master this topic.
Cloud Firestore is a flexible, scalable NoSQL document database that stores data in documents and collections with real-time sync, offline support, and automatic scaling.
What You'll Learn
How to model data in Firestore, perform CRUD operations, use real-time listeners, batch writes, transactions, and offline persistence — building a durable data layer without managing a database server.
Why It Matters
Firestore eliminates database server management, provides real-time sync out of the box, and scales automatically. DodaTech's Durga Antivirus Pro stores device configurations, scan history, and threat alerts in Firestore — serving 500K+ devices with zero database administration.
Real-World Use
A security dashboard that shows live threat data: each device writes scan results to Firestore, the dashboard listens for real-time updates, and administrators query historical data with filters and pagination.
flowchart LR
A["Mobile App\nWrite Scan Result"] --> B["Firestore\nCollection"]
B --> C["Real-time Listener\nDashboard"]
B --> D["Cloud Functions\nProcess Alert"]
B --> E["Analytics Query\nHistorical Data"]
style B fill:#dbeafe,stroke:#2563eb
style C fill:#bbf7d0,stroke:#16a34a
Understanding the Data Model
Firestore organizes data in a hierarchy: collections contain documents, and documents contain fields (key-value pairs). Documents can also contain subcollections.
Think of a collection as a folder and a document as a file with structured data. Unlike SQL tables, each document in a collection can have different fields — this is called schema flexibility.
// Document structure for a device scan record
const scanRecord = {
deviceId: "dev_abc123",
userId: "user_xyz789",
scanDate: new Date("2026-06-28"),
threatsFound: 3,
status: "completed",
threatList: [
{ name: "Trojan.Generic", severity: "high" },
{ name: "Adware.Bundle", severity: "low" }
]
};
Why this model? Firestore documents are JSON-like objects. Arrays, nested objects, and timestamps are native types. This matches how your application code structures data — no ORM needed.
Writing Documents
import { doc, setDoc, addDoc, collection } from "firebase/firestore";
// setDoc: Write with a known ID
async function writeWithKnownId() {
const deviceRef = doc(db, "devices", "dev_abc123");
await setDoc(deviceRef, {
name: "Alice Laptop",
os: "Windows 11",
lastScan: new Date(),
userId: "user_xyz789"
});
console.log("Device saved with ID: dev_abc123");
}
// Expected output: Device saved with ID: dev_abc123
// addDoc: Auto-generate ID
async function addScanRecord(deviceId) {
const scansRef = collection(db, "devices", deviceId, "scans");
const docRef = await addDoc(scansRef, {
timestamp: new Date(),
threats: 2,
status: "clean"
});
console.log("Scan recorded with ID:", docRef.id);
}
// Expected output: Scan recorded with ID: ABC123xyz789
Reading Documents
import { getDoc, getDocs, collection, query, where } from "firebase/firestore";
// Get a single document
async function getDevice(deviceId) {
const docSnap = await getDoc(doc(db, "devices", deviceId));
if (docSnap.exists()) {
console.log("Device data:", docSnap.data());
} else {
console.log("No device found");
}
}
// Expected output: Device data: { name: "Alice Laptop", os: "Windows 11", ... }
// Query a collection
async function findDevicesByUser(userId) {
const q = query(collection(db, "devices"), where("userId", "==", userId));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
console.log(doc.id, "=>", doc.data().name);
});
}
// Expected output: dev_abc123 => Alice Laptop
// dev_def456 => Bob Desktop
Real-time Listener
import { onSnapshot, doc } from "firebase/firestore";
function listenToDevice(deviceId) {
const deviceRef = doc(db, "devices", deviceId);
const unsubscribe = onSnapshot(deviceRef, (doc) => {
if (doc.exists()) {
console.log("Device updated:", doc.data());
}
});
// Call unsubscribe() when done listening
return unsubscribe;
}
// Expected output (when data changes): Device updated: { name: "Alice Laptop", ... }
Batch Writes and Transactions
import { writeBatch, doc } from "firebase/firestore";
async function updateMultipleDevices() {
const batch = writeBatch(db);
batch.update(doc(db, "devices", "dev_abc123"), { status: "inactive" });
batch.update(doc(db, "devices", "dev_def456"), { status: "inactive" });
await batch.commit();
console.log("Batch write completed — both devices updated atomically");
}
// Expected output: Batch write completed — both devices updated atomically
Offline Persistence
import { enableMultiTabIndexedDbPersistence } from "firebase/firestore";
async function enableOffline() {
try {
await enableMultiTabIndexedDbPersistence(db);
console.log("Offline persistence enabled — data works without internet");
} catch (err) {
if (err.code === "failed-precondition") {
console.log("Multiple tabs open — persistence in one tab only");
}
}
}
// Expected output: Offline persistence enabled — data works without internet
Common Mistakes
1. Creating Deeply Nested Data
Firestore documents have a 1 MiB size limit. Deeply nested objects make partial updates difficult and increase read costs. Flatten data or use subcollections.
2. Using Arrays for Large Lists
Arrays in documents have a 20 KiB limit and cannot be queried efficiently. Use subcollections for lists that grow beyond a few items.
3. Forgetting to Handle Document Existence
Always check docSnap.exists() before reading data. Accessing non-existent documents returns undefined and causes runtime errors.
4. Ignoring Write Limits
Firestore limits writes to 1 per second per document. Bulk updates to the same document cause contention. Use batch writes for multiple documents.
5. Not Using Subcollections for One-to-Many
Storing a list of scan IDs inside a device document causes growth issues. Use subcollections like devices/{id}/scans for one-to-many relationships.
Practice Questions
- What is the difference between
setDocandaddDoc? - When should you use a subcollection instead of a nested array?
- What happens when you write to a document faster than 1 write per second?
- How does offline persistence work with multi-tab support?
Answers:
setDocwrites to a known document ID;addDocauto-generates a unique ID.- Use subcollections for one-to-many relationships that grow (scans, messages, comments). Nested arrays are for small, fixed-size lists.
- Writes beyond 1 per second cause contention errors. Use batch writes or retry logic.
- Each tab maintains its own cache.
enableMultiTabIndexedDbPersistencesynchronizes across tabs.
Challenge: Model Durga Antivirus Pro's data: a users/{id}/devices/{id}/scans/{id} hierarchy. Write a batch that records a new scan for all devices of a user after a mass threat detection.
FAQ
Mini Project
Build a device management system with Firestore: create a devices collection, write scan results to a subcollection, set up a real-time listener for the dashboard, and test offline persistence by disconnecting the network.
What's Next
Firestore Queries & Indexes — filter, sort, and paginate your Firestore data efficiently.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro