Cloud  /  Google Cloud

GCP Google Cloud Platform 25 guides · updated 2026

Guides to BigQuery, Vertex AI, GKE, Dataflow, and the rest of Google's data- and AI-first cloud — written for engineers shipping real workloads.

Google Cloud Firestore: Serverless Document Database for Web and Mobile Apps

Firestore is a document database that eliminates infrastructure management entirely. You do not provision nodes, configure replication, or set up connection pools. You write documents to collections, attach real-time listeners, and Firestore pushes updates to all connected clients automatically. The database scales to accommodate any traffic level without configuration changes.

This guide covers the data model, real-time listeners, offline support, security rules, composite indexes for complex queries, and the difference between Firestore in Native mode and Datastore mode.


The Data Model: Collections and Documents

Firestore organizes data as documents nested inside collections. A document is a JSON-like object with typed fields. Documents live inside collections, and documents can contain subcollections.

Firestore database
├── collection: "users"
│ ├── document: "user_abc123"
│ │ ├── name: "Alice Chen"
│ │ ├── email: "alice@example.com"
│ │ ├── created_at: Timestamp(2025-01-15T09:00:00Z)
│ │ └── subcollection: "orders"
│ │ ├── document: "order_001"
│ │ │ ├── total: 89.99
│ │ │ └── status: "shipped"
│ │ └── document: "order_002"
│ │ ├── total: 124.50
│ │ └── status: "pending"
│ └── document: "user_def456"
│ └── ...
└── collection: "products"
└── ...

Unlike a relational database, you do not define a schema in advance. Different documents in the same collection can have different fields. Firestore supports these field types: string, number, boolean, null, timestamp, geopoint, reference, array, and map.


Reading and Writing Documents

The Firebase SDK is the standard way to interact with Firestore from web and mobile applications. The Admin SDK (server-side) and the Cloud Client Library handle backend operations.

// Web SDK (Firebase v9 modular API)
import { initializeApp } from "firebase/app";
import {
getFirestore,
doc,
setDoc,
getDoc,
collection,
query,
where,
getDocs,
onSnapshot,
} from "firebase/firestore";
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
// Write a document
await setDoc(doc(db, "users", "user_abc123"), {
name: "Alice Chen",
email: "alice@example.com",
tier: "premium",
createdAt: new Date(),
});
// Read a single document
const docRef = doc(db, "users", "user_abc123");
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
console.log("User data:", docSnap.data());
}
// Query documents
const q = query(
collection(db, "users"),
where("tier", "==", "premium"),
where("createdAt", ">=", new Date("2025-01-01"))
);
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
console.log(doc.id, doc.data());
});

Real-Time Listeners: The Core Differentiator

Firestore’s real-time listener capability is what makes it compelling for mobile and collaborative applications. When data changes in the database, Firestore pushes the update to all clients listening on that document or query.

// Listen to a document — fires on every change
const unsubscribe = onSnapshot(
doc(db, "orders", "order_001"),
(docSnap) => {
console.log("Order updated:", docSnap.data());
// Update UI automatically
}
);
// Listen to a query — fires whenever any matching document changes
const q = query(
collection(db, "orders"),
where("assignedDriver", "==", "driver_007")
);
const unsubscribeQuery = onSnapshot(q, (querySnapshot) => {
querySnapshot.docChanges().forEach((change) => {
if (change.type === "added") console.log("New order:", change.doc.data());
if (change.type === "modified") console.log("Updated:", change.doc.data());
if (change.type === "removed") console.log("Removed:", change.doc.id);
});
});
// Stop listening when component unmounts
unsubscribe();

This push model means your UI reflects database state without polling. A ride-sharing app showing driver location, a collaborative document editor, or a live sports scoreboard all benefit from this pattern.


Offline Support

Firestore SDKs for iOS, Android, and web cache data locally. When the device loses connectivity, reads serve from the local cache and writes queue locally. When connectivity restores, queued writes sync to the server automatically.

// Enable offline persistence (web)
import { enableIndexedDbPersistence } from "firebase/firestore";
enableIndexedDbPersistence(db).catch((err) => {
if (err.code === "failed-precondition") {
// Multiple tabs open — persistence only works in one tab at a time
}
});
// Check connection state
import { onSnapshotsInSync } from "firebase/firestore";
onSnapshotsInSync(db, () => {
console.log("All listeners in sync with backend");
});

This offline capability makes Firestore well-suited for mobile apps that need to work in areas with poor connectivity — field service apps, inspection tools, or delivery applications.


Security Rules: Database-Level Authorization

Firestore Security Rules define who can read and write which documents. Rules run on the Firestore server and cannot be bypassed by client code.

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can only read/write their own profile
match /users/{userId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
}
// Orders: users can read their own, admins can read all
match /orders/{orderId} {
allow read: if request.auth != null
&& (resource.data.userId == request.auth.uid
|| request.auth.token.admin == true);
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid
&& request.resource.data.total > 0;
allow update: if request.auth.token.admin == true;
}
// Product catalog is publicly readable
match /products/{productId} {
allow read: if true;
allow write: if request.auth.token.admin == true;
}
}
}

Security rules use request.auth (the authenticated user’s token), request.resource.data (the data being written), and resource.data (existing document data). They replace backend authorization middleware for client-facing applications.


Composite Indexes for Complex Queries

Firestore automatically creates single-field indexes. When you query with multiple filters or combine a filter with an order-by clause, Firestore needs a composite index. Without it, the query returns an error with a link to create the required index.

Query: where("status", "==", "pending")
.where("region", "==", "EMEA")
.orderBy("createdAt", "desc")
Required composite index:
Collection: orders
Fields: status ASC, region ASC, createdAt DESC

Create composite indexes in the Firebase console, via the firebase.json configuration, or by clicking the link in the error message during development.

{
"indexes": [
{
"collectionGroup": "orders",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "status", "order": "ASCENDING" },
{ "fieldPath": "region", "order": "ASCENDING" },
{ "fieldPath": "createdAt", "order": "DESCENDING" }
]
}
]
}

Firestore Native Mode vs Datastore Mode

Firestore runs in two modes:

┌─────────────────────────────┬──────────────────────────────────────────────────┐
│ Native Mode │ Datastore Mode │
├─────────────────────────────┼──────────────────────────────────────────────────┤
│ Supports real-time listeners│ No real-time listeners │
│ Web/mobile SDKs available │ Server-side only (Datastore API) │
│ Offline sync │ No offline sync │
│ Security rules │ Cloud IAM only │
│ New projects should use │ Legacy projects migrating from Datastore │
└─────────────────────────────┴──────────────────────────────────────────────────┘

New projects should use Native mode. Datastore mode exists for backward compatibility with applications built on Cloud Datastore before Firestore replaced it.


Transactions and Batch Writes

Firestore transactions read and write atomically. If any document read inside the transaction has been modified by another client before the transaction commits, Firestore retries the transaction automatically.

import { runTransaction, writeBatch } from "firebase/firestore";
// Transaction: read-then-write pattern
await runTransaction(db, async (transaction) => {
const stockRef = doc(db, "inventory", "PROD-001");
const stockDoc = await transaction.get(stockRef);
if (!stockDoc.exists()) throw new Error("Product not found");
const currentStock = stockDoc.data().quantity;
if (currentStock < 1) throw new Error("Out of stock");
transaction.update(stockRef, { quantity: currentStock - 1 });
transaction.set(doc(collection(db, "orders")), {
productId: "PROD-001",
userId: currentUser.uid,
quantity: 1,
status: "confirmed",
createdAt: new Date(),
});
});
// Batch write: multiple writes committed atomically (no reads)
const batch = writeBatch(db);
batch.set(doc(db, "stats", "daily"), { date: today, orders: count });
batch.update(doc(db, "users", userId), { lastOrderAt: new Date() });
await batch.commit();

Summary

Firestore’s strength is in client-facing applications where real-time updates, offline support, and client-side authorization rules simplify the architecture significantly. Instead of building an API layer to handle authorization and push updates to clients, Firestore handles both natively. The trade-offs are query limitations (no ad-hoc joins, no aggregate queries without summary documents) and a pricing model based on reads, writes, and deletes — heavy-read dashboards can become expensive. For those access patterns, export data to BigQuery and query it there. Firestore excels as the operational data store for web and mobile apps; BigQuery excels as the analytical layer.