Skip to main content
Technology & EngineeringGcp Services247 lines

Firestore

Model, query, and manage data with Google Cloud Firestore NoSQL document database

Quick Summary31 lines
You are an expert in Google Cloud Firestore for building scalable applications with a flexible NoSQL document database.

## Key Points

- **Using auto-incrementing document IDs** -- Sequential IDs concentrate writes on a single storage partition, creating hot spots. Use Firestore's auto-generated IDs or add random prefixes.
- Document-collection data model with nested subcollections
- Real-time snapshot listeners for live data synchronization
- Strong consistency for all reads
- Multi-region replication and automatic scaling
- Offline support for mobile and web clients
- Integration with Firebase SDKs, Cloud Functions triggers, and Security Rules
- **Design data for your queries.** Firestore does not support arbitrary joins. Denormalize data and use subcollections to match your access patterns.
- **Use composite indexes proactively.** Firestore requires a composite index for queries with multiple filters or mixed ordering. Define them in `firestore.indexes.json`.
- **Prefer batch writes for bulk operations.** A batch can hold up to 500 operations and executes atomically.
- **Use server timestamps.** Always use `FieldValue.serverTimestamp()` instead of client-generated dates for consistency.
- **Limit document size.** A single document is capped at 1 MiB. Store large blobs in Cloud Storage and reference them by URL.

## Quick Example

```javascript
const { Firestore } = require('@google-cloud/firestore');
const db = new Firestore({ projectId: 'my-project' });
```

```python
from google.cloud import firestore

db = firestore.Client(project="my-project")
```
skilldb get gcp-services-skills/FirestoreFull skill: 247 lines
Paste into your CLAUDE.md or agent config

GCP Service — Firestore

You are an expert in Google Cloud Firestore for building scalable applications with a flexible NoSQL document database.

Core Philosophy

Firestore is a document database designed for application-centric data access. Unlike relational databases where you normalize data and join at query time, Firestore requires you to model data around your queries. If your UI displays a user's profile alongside their recent orders, store them in a way that can be fetched with one or two reads -- not a chain of collection scans. Denormalization is not a compromise in Firestore; it is the correct modeling strategy.

Real-time listeners are Firestore's most powerful feature and should be used deliberately. Snapshot listeners push changes to connected clients instantly, enabling live dashboards, collaborative editing, and chat with no polling. But every active listener holds an open connection and incurs read charges on every matching change. Use listeners for data that genuinely needs to be live and use regular gets for data that does not change during a user session.

Security rules are your server-side validation layer for client-side SDKs. Deploying with allow read, write: if true is equivalent to giving every user admin access to your database. Write granular rules that validate document structure, check authentication state, and enforce ownership constraints. For server-side SDKs (Admin SDK), security rules do not apply -- but that is not an excuse to skip them for client-facing applications.

Anti-Patterns

  • Fetching entire collections without filters or pagination -- Reading all documents in a large collection is expensive and slow. Always use WHERE clauses, limit results, and paginate with cursors.
  • Designing deeply normalized data that requires multiple reads -- If displaying a single screen requires reading from five collections, the data model is wrong. Denormalize and colocate data that is read together.
  • Deploying with open security rules in production -- Rules like allow read, write: if true expose your entire database to any authenticated or unauthenticated user. Define granular per-collection rules.
  • Using auto-incrementing document IDs -- Sequential IDs concentrate writes on a single storage partition, creating hot spots. Use Firestore's auto-generated IDs or add random prefixes.
  • Ignoring composite index requirements -- Queries that combine filters on multiple fields fail at runtime if the required composite index does not exist. Define indexes proactively in firestore.indexes.json.

Overview

Firestore is a serverless, fully managed NoSQL document database that scales automatically. It stores data in documents organized into collections, supports real-time listeners, offline persistence, and ACID transactions.

Key capabilities:

  • Document-collection data model with nested subcollections
  • Real-time snapshot listeners for live data synchronization
  • Strong consistency for all reads
  • Multi-region replication and automatic scaling
  • Offline support for mobile and web clients
  • Integration with Firebase SDKs, Cloud Functions triggers, and Security Rules

Setup & Configuration

Enable the API and create a database

gcloud services enable firestore.googleapis.com

# Create a Firestore database (Native mode)
gcloud firestore databases create \
  --location=us-central1 \
  --type=firestore-native

Install client libraries

# Node.js
npm install @google-cloud/firestore

# Python
pip install google-cloud-firestore

# Go
go get cloud.google.com/go/firestore

Initialize the client (Node.js)

const { Firestore } = require('@google-cloud/firestore');
const db = new Firestore({ projectId: 'my-project' });

Initialize the client (Python)

from google.cloud import firestore

db = firestore.Client(project="my-project")

Core Patterns

Create and set documents

// Auto-generated ID
const docRef = await db.collection('users').add({
  name: 'Alice',
  email: 'alice@example.com',
  createdAt: Firestore.FieldValue.serverTimestamp(),
});

// Explicit ID
await db.collection('users').doc('alice-id').set({
  name: 'Alice',
  email: 'alice@example.com',
});

// Merge (partial update without overwriting)
await db.collection('users').doc('alice-id').set(
  { lastLogin: Firestore.FieldValue.serverTimestamp() },
  { merge: true }
);

Read documents

// Single document
const doc = await db.collection('users').doc('alice-id').get();
if (doc.exists) {
  console.log(doc.data());
}

// Query a collection
const snapshot = await db.collection('users')
  .where('email', '==', 'alice@example.com')
  .limit(10)
  .get();

snapshot.forEach(doc => console.log(doc.id, doc.data()));

Compound queries and ordering

# Python: compound query with ordering
docs = (
    db.collection("orders")
    .where("status", "==", "active")
    .where("total", ">=", 100)
    .order_by("total", direction=firestore.Query.DESCENDING)
    .limit(20)
    .stream()
)

for doc in docs:
    print(f"{doc.id} => {doc.to_dict()}")

Pagination with cursors

const first = await db.collection('products')
  .orderBy('price')
  .limit(25)
  .get();

const lastDoc = first.docs[first.docs.length - 1];

const next = await db.collection('products')
  .orderBy('price')
  .startAfter(lastDoc)
  .limit(25)
  .get();

Transactions

const userRef = db.collection('accounts').doc('user-1');

await db.runTransaction(async (transaction) => {
  const doc = await transaction.get(userRef);
  const currentBalance = doc.data().balance;

  if (currentBalance < 50) {
    throw new Error('Insufficient funds');
  }

  transaction.update(userRef, { balance: currentBalance - 50 });
});

Batch writes

const batch = db.batch();

batch.set(db.collection('users').doc('u1'), { name: 'Alice' });
batch.update(db.collection('users').doc('u2'), { active: true });
batch.delete(db.collection('users').doc('u3'));

await batch.commit();

Real-time listeners (Firebase Web SDK)

import { onSnapshot, collection, query, where } from 'firebase/firestore';

const q = query(
  collection(db, 'messages'),
  where('roomId', '==', 'room-1')
);

const unsubscribe = onSnapshot(q, (snapshot) => {
  snapshot.docChanges().forEach((change) => {
    if (change.type === 'added') console.log('New:', change.doc.data());
    if (change.type === 'modified') console.log('Updated:', change.doc.data());
    if (change.type === 'removed') console.log('Removed:', change.doc.data());
  });
});

Composite indexes (firestore.indexes.json)

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

Deploy indexes:

gcloud firestore indexes composite create \
  --collection-group=orders \
  --field-config field-path=status,order=ascending \
  --field-config field-path=total,order=descending

Best Practices

  • Design data for your queries. Firestore does not support arbitrary joins. Denormalize data and use subcollections to match your access patterns.
  • Use composite indexes proactively. Firestore requires a composite index for queries with multiple filters or mixed ordering. Define them in firestore.indexes.json.
  • Prefer batch writes for bulk operations. A batch can hold up to 500 operations and executes atomically.
  • Use server timestamps. Always use FieldValue.serverTimestamp() instead of client-generated dates for consistency.
  • Limit document size. A single document is capped at 1 MiB. Store large blobs in Cloud Storage and reference them by URL.
  • Use collection group queries for subcollections. Query across all subcollections with the same name using collectionGroup().

Common Pitfalls

  • Missing composite indexes. Queries that combine filters on multiple fields fail at runtime if the required index does not exist. Check error messages for the index creation link.
  • Hot spots from sequential document IDs. Auto-incrementing IDs concentrate writes on a single partition. Use Firestore's auto-generated IDs or randomized prefixes.
  • Reading entire collections. Fetching all documents in a large collection is expensive. Always use filters and pagination.
  • Exceeding transaction limits. A transaction can read up to 10,000 documents and modify up to 500. Design data to keep transaction scopes small.
  • Ignoring security rules in production. Deploying with open rules (allow read, write: if true) is a security risk. Always define granular rules.
  • Not handling offline/cache behavior. On mobile clients, Firestore may return cached data. Check metadata.fromCache when freshness matters.

Install this skill directly: skilldb add gcp-services-skills

Get CLI access →