← Back to DevBytes

Firestore: Complete Setup and Configuration Guide

What is Firestore?

Firestore is a NoSQL cloud database from Google Firebase, designed for mobile, web, and server application development. It stores data in flexible, hierarchical documents organized into collections, offering real-time synchronization and offline persistence out of the box. Unlike Firebase Realtime Database, Firestore supports more expressive queries, deeper data nesting, and automatic multi-region replication for stronger reliability.

Core Concepts

Firestore organizes data into documents and collections. A document is a lightweight record containing key-value pairs, similar to a JSON object. Documents live inside collections, which act as logical containers. Documents can also contain subcollections, enabling hierarchical data structures without deep nesting in a single document.

Why Firestore Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Firestore solves fundamental challenges that developers face when building modern applications. Its architecture provides a unique combination of features that make it indispensable for real-time, collaborative, and offline-first applications.

Real-Time Sync Without Infrastructure

Firestore's real-time listeners automatically push data changes to connected clients within milliseconds. There is no need to manage WebSocket servers, handle connection state, or write synchronization logic. A single onSnapshot() call delivers live updates with built-in retry logic and connection management.

Offline Persistence Built In

Firestore SDKs cache data locally by default on mobile and web platforms. Users can read, write, and query data while offline, and changes automatically sync when connectivity returns. This eliminates the complexity of managing local databases and conflict resolution for most use cases.

Scalable Without Operational Overhead

Firestore scales horizontally based on document read/write load, not server provisioning. You never need to configure sharding, manage index rebuilds, or plan capacity. The pricing model charges per operation, which aligns cost directly with usage — ideal for startups and enterprise applications alike.

Security Rules for Data Access Control

Firestore security rules allow you to define access control logic that runs at Google's edge before data touches your client code. Rules can validate document structure, enforce role-based access, and restrict queries — all without a custom backend API layer.

Setting Up Firestore: Complete Configuration Guide

Step 1: Create a Firebase Project

Navigate to the Firebase Console, click "Add project," and follow the creation wizard. Enable Google Analytics if desired, then create the project. Once the project exists, you'll land on the project overview dashboard.

Step 2: Enable Firestore

In the Firebase Console sidebar, click Firestore Database under the "Build" section. Click "Create database." You will be prompted to choose between production mode and test mode:

Select a location for your database. This is a permanent decision that determines where your data is physically stored. Choose the region closest to your user base. Multi-region locations (like nam5 or eur3) offer higher availability but come with higher costs.

# Firestore location options (selected during creation)
# Single-region examples:
#   us-central1, us-east1, europe-west1, asia-southeast1

# Multi-region examples:
#   nam5 (United States multi-region)
#   eur3 (Europe multi-region)

Step 3: Add Firebase SDK to Your Project

Firestore supports multiple platforms. Below are the complete setup instructions for the most common environments.

Web (JavaScript/TypeScript)

Install Firebase via npm and initialize it in your application entry point. The modular SDK (v9+) offers tree-shakeable imports for smaller bundle sizes.

# Install Firebase SDK
npm install firebase

# Or with yarn
yarn add firebase
// firebaseConfig.js — Create this file in your project root
import { initializeApp } from 'firebase/app';
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';

const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_PROJECT_ID.appspot.com",
  messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
  appId: "YOUR_APP_ID"
};

// Initialize Firebase App
const app = initializeApp(firebaseConfig);

// Initialize Firestore
const db = getFirestore(app);

// Optional: Connect to local emulator during development
if (process.env.NODE_ENV === 'development') {
  connectFirestoreEmulator(db, 'localhost', 8080);
}

export { db };

React Native

For React Native projects, use the React Native Firebase wrapper which provides native SDK bindings for better performance on iOS and Android.

# Install React Native Firebase
npm install @react-native-firebase/app @react-native-firebase/firestore

# iOS — link native modules
cd ios && pod install
// App.js or app/index.js
import firebase from '@react-native-firebase/app';
import firestore from '@react-native-firebase/firestore';

// Initialize if not already done
if (!firebase.apps.length) {
  firebase.initializeApp({
    apiKey: "YOUR_API_KEY",
    projectId: "YOUR_PROJECT_ID",
    appId: "YOUR_APP_ID"
  });
}

// Firestore is available as firestore()
const db = firestore();

export { db };

Node.js / Admin SDK (Backend)

For server-side applications, use the Firebase Admin SDK which bypasses security rules and provides privileged access to Firestore.

# Install Admin SDK
npm install firebase-admin
// admin.js — Backend initialization
const admin = require('firebase-admin');
const { getFirestore } = require('firebase-admin/firestore');

// Option 1: Using a service account key file
const serviceAccount = require('./service-account-key.json');

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

// Option 2: Using application default credentials (Cloud Functions, GCP)
// admin.initializeApp(); // Automatically discovers credentials

// Initialize Firestore
const db = getFirestore();

// Configure settings
db.settings({
  ignoreUndefinedProperties: true,  // Skip undefined values in writes
  preferRest: false                 // Use gRPC for better performance
});

module.exports = { db, admin };

Python Admin SDK

# Install Firebase Admin SDK for Python
pip install firebase-admin
# firestore_init.py
import firebase_admin
from firebase_admin import credentials, firestore

# Initialize the app with a service account
cred = credentials.Certificate('service-account-key.json')
firebase_admin.initialize_app(cred)

# Get a Firestore client
db = firestore.client()

# Optional: Configure Firestore settings
db.settings({
    'ignore_undefined_properties': True
})

Step 4: Configure Firestore Settings

Firestore offers several configuration options that control behavior at the SDK level. These settings are applied per-client instance and do not affect other connections.

// Web SDK — Firestore settings configuration
import { getFirestore, initializeFirestore, memoryLocalCache, persistentLocalCache } from 'firebase/firestore';

// Option 1: Standard initialization with default settings
const db = getFirestore(app);

// Option 2: Initialize with custom cache settings
const db = initializeFirestore(app, {
  localCache: persistentLocalCache({
    cacheSizeBytes: 100 * 1024 * 1024  // 100 MB cache
  })
});

// Option 3: Memory-only cache (no disk persistence)
const memoryDb = initializeFirestore(app, {
  localCache: memoryLocalCache()
});

// Setting snapshot listener options
import { onSnapshot, collection, query } from 'firebase/firestore';

// Apply snapshot listener options per query
const unsubscribe = onSnapshot(
  query(collection(db, 'users')),
  {
    includeMetadataChanges: false,  // Only trigger on server-confirmed changes
    source: 'default'               // 'default', 'server', or 'cache'
  },
  (snapshot) => {
    console.log('Snapshot received:', snapshot.docs.length, 'documents');
  }
);
// Admin SDK settings (Node.js)
db.settings({
  ignoreUndefinedProperties: true,   // Ignore undefined fields instead of throwing
  preferRest: false,                 // Use gRPC protocol (default)
  // Maximum number of attempts for failed operations
  maxRetryAttempts: 5
});

Step 5: Security Rules Configuration

Security rules are defined in the Firebase Console or deployed via the Firebase CLI. They control read and write access to documents based on authentication state, data content, and request characteristics.

// firestore.rules — Deploy via Firebase CLI or Console
rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    
    // Global helper functions
    function isAuthenticated() {
      return request.auth != null;
    }
    
    function isOwner(userId) {
      return request.auth.uid == userId;
    }
    
    // Users collection rules
    match /users/{userId} {
      // Allow read if authenticated
      allow read: if isAuthenticated();
      
      // Allow write only if user owns the document
      // AND the update doesn't modify privileged fields
      allow create, update: if isOwner(userId) 
        && !request.resource.data.diff(resource.data).hasField('role');
      
      // Prevent deletion entirely
      allow delete: if false;
    }
    
    // User-specific subcollection
    match /users/{userId}/posts/{postId} {
      allow read: if isAuthenticated();
      allow create, update, delete: if isOwner(userId);
      
      // Validate post data structure on create
      allow create: if request.resource.data.keys().hasAll(['title', 'content'])
        && request.resource.data.title is string
        && request.resource.data.title.size() <= 200;
    }
    
    // Public collection accessible to all authenticated users
    match /public_content/{docId} {
      allow read: if isAuthenticated();
      allow write: if isAuthenticated() 
        && request.resource.data.authorId == request.auth.uid;
    }
  }
}

Step 6: Deploy Security Rules via CLI

The Firebase CLI provides a streamlined workflow for managing and deploying security rules across environments.

# Install Firebase CLI
npm install -g firebase-tools

# Login to Firebase
firebase login

# Initialize Firebase in your project
firebase init firestore

# Deploy security rules only
firebase deploy --only firestore:rules

# Deploy rules and indexes together
firebase deploy --only firestore

# Deploy rules with a specific target (for multiple environments)
firebase deploy --only firestore:rules --project=my-project-dev

Step 7: Create Composite Indexes

Firestore automatically indexes individual fields, but complex queries combining multiple fields with range filters require composite indexes. The Firebase Console provides an index management interface, and the CLI allows you to define indexes as code.

// firestore.indexes.json — Deploy alongside rules
{
  "indexes": [
    {
      "collectionGroup": "posts",
      "queryScope": "COLLECTION",
      "fields": [
        {
          "fieldPath": "authorId",
          "order": "ASCENDING"
        },
        {
          "fieldPath": "createdAt",
          "order": "DESCENDING"
        }
      ]
    },
    {
      "collectionGroup": "orders",
      "queryScope": "COLLECTION",
      "fields": [
        {
          "fieldPath": "status",
          "order": "ASCENDING"
        },
        {
          "fieldPath": "total",
          "order": "DESCENDING"
        },
        {
          "fieldPath": "createdAt",
          "order": "DESCENDING"
        }
      ]
    }
  ],
  "fieldOverrides": [
    {
      "collectionGroup": "users",
      "fieldPath": "email",
      "indexes": [
        {
          "queryScope": "COLLECTION_GROUP",
          "order": "ASCENDING"
        }
      ]
    }
  ]
}
# Deploy indexes
firebase deploy --only firestore:indexes

How to Use Firestore: Practical Examples

Basic CRUD Operations

Adding Documents

Firestore provides two methods for adding documents: auto-generated IDs and custom IDs. Choose auto-generated IDs when you don't need predictable document keys; use custom IDs for entities like users where the ID carries meaning.

// Web SDK — Adding documents
import { collection, addDoc, setDoc, doc } from 'firebase/firestore';
import { db } from './firebaseConfig';

// Auto-generated document ID
async function createPost(title, content, authorId) {
  const docRef = await addDoc(collection(db, 'posts'), {
    title: title,
    content: content,
    authorId: authorId,
    createdAt: new Date(),
    status: 'published',
    likes: 0
  });
  console.log('Document written with ID:', docRef.id);
  return docRef.id;
}

// Custom document ID (e.g., for users)
async function createUserProfile(userId, profileData) {
  await setDoc(doc(db, 'users', userId), {
    displayName: profileData.displayName,
    email: profileData.email,
    createdAt: new Date(),
    updatedAt: new Date()
  });
}

// Merging data into an existing document (doesn't overwrite unspecified fields)
async function updateUserPartial(userId, updates) {
  await setDoc(doc(db, 'users', userId), updates, { merge: true });
}

Reading Documents

// Web SDK — Reading documents
import { getDoc, getDocs, doc, collection } from 'firebase/firestore';

// Read a single document
async function getUserProfile(userId) {
  const docSnap = await getDoc(doc(db, 'users', userId));
  
  if (docSnap.exists()) {
    console.log('User data:', docSnap.data());
    return { id: docSnap.id, ...docSnap.data() };
  } else {
    console.log('No such document');
    return null;
  }
}

// Read all documents in a collection
async function getAllPosts() {
  const querySnapshot = await getDocs(collection(db, 'posts'));
  const posts = [];
  
  querySnapshot.forEach((doc) => {
    posts.push({ id: doc.id, ...doc.data() });
  });
  
  console.log(`Retrieved ${posts.length} posts`);
  return posts;
}

Updating Documents

// Web SDK — Updating documents
import { updateDoc, doc, increment, arrayUnion, arrayRemove, serverTimestamp } from 'firebase/firestore';

// Update specific fields
async function updatePostStatus(postId, newStatus) {
  const docRef = doc(db, 'posts', postId);
  await updateDoc(docRef, {
    status: newStatus,
    updatedAt: serverTimestamp()
  });
}

// Atomic numeric increments (no race conditions)
async function incrementLikeCount(postId) {
  const docRef = doc(db, 'posts', postId);
  await updateDoc(docRef, {
    likes: increment(1)
  });
}

// Add item to an array field
async function addTagToPost(postId, tag) {
  const docRef = doc(db, 'posts', postId);
  await updateDoc(docRef, {
    tags: arrayUnion(tag)
  });
}

// Remove item from an array field
async function removeTagFromPost(postId, tag) {
  const docRef = doc(db, 'posts', postId);
  await updateDoc(docRef, {
    tags: arrayRemove(tag)
  });
}

Deleting Documents

// Web SDK — Deleting documents
import { deleteDoc, doc } from 'firebase/firestore';

async function deletePost(postId) {
  const docRef = doc(db, 'posts', postId);
  await deleteDoc(docRef);
  console.log(`Post ${postId} deleted`);
}

// Note: Deleting a document does NOT delete its subcollections
// You must recursively delete subcollection documents first
async function deleteUserWithSubcollections(userId) {
  // Delete all posts in subcollection
  const postsSnapshot = await getDocs(
    collection(db, 'users', userId, 'posts')
  );
  
  const batch = [];
  postsSnapshot.forEach((doc) => {
    batch.push(deleteDoc(doc.ref));
  });
  
  await Promise.all(batch);
  
  // Then delete the user document
  await deleteDoc(doc(db, 'users', userId));
}

Querying Data

Firestore queries combine field filters, ordering, and limits to retrieve specific subsets of documents. Queries are always indexed-based — Firestore will reject queries that lack a corresponding index with a clear error message and a direct link to create the required index.

// Web SDK — Query examples
import { query, where, orderBy, limit, startAfter, getDocs } from 'firebase/firestore';

// Simple equality filter
async function getPostsByAuthor(authorId) {
  const q = query(
    collection(db, 'posts'),
    where('authorId', '==', authorId),
    orderBy('createdAt', 'desc')
  );
  
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}

// Compound query with multiple filters
async function getPublishedPostsByAuthor(authorId) {
  const q = query(
    collection(db, 'posts'),
    where('authorId', '==', authorId),
    where('status', '==', 'published'),
    orderBy('createdAt', 'desc'),
    limit(20)
  );
  
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}

// Range filter
async function getRecentPosts(daysAgo = 7) {
  const cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - daysAgo);
  
  const q = query(
    collection(db, 'posts'),
    where('createdAt', '>=', cutoffDate),
    orderBy('createdAt', 'desc')
  );
  
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}

// Pagination using cursor
async function getPaginatedPosts(pageSize = 10, lastDoc = null) {
  let q = query(
    collection(db, 'posts'),
    orderBy('createdAt', 'desc'),
    limit(pageSize)
  );
  
  if (lastDoc) {
    q = query(q, startAfter(lastDoc));
  }
  
  const snapshot = await getDocs(q);
  const posts = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
  
  // Return the last document for the next page request
  const lastVisible = snapshot.docs[snapshot.docs.length - 1];
  
  return {
    posts,
    lastVisible,
    hasMore: snapshot.docs.length === pageSize
  };
}

Real-Time Listeners

Real-time listeners are Firestore's most powerful feature. They maintain an active connection and push changes to the client as they occur. Listeners can be attached to individual documents, collections, or query results.

// Web SDK — Real-time listeners
import { onSnapshot, doc, collection, query, where } from 'firebase/firestore';

// Listen to a single document
function subscribeToUser(userId, callback) {
  const unsubscribe = onSnapshot(
    doc(db, 'users', userId),
    (docSnapshot) => {
      if (docSnapshot.exists()) {
        callback({ id: docSnapshot.id, ...docSnapshot.data() });
      } else {
        callback(null); // Document was deleted
      }
    },
    (error) => {
      console.error('Listener error:', error);
    }
  );
  
  // Return unsubscribe function for cleanup
  return unsubscribe;
}

// Listen to a query
function subscribeToUserPosts(userId, callback) {
  const q = query(
    collection(db, 'posts'),
    where('authorId', '==', userId),
    orderBy('createdAt', 'desc'),
    limit(50)
  );
  
  const unsubscribe = onSnapshot(q, (querySnapshot) => {
    const posts = [];
    querySnapshot.forEach((doc) => {
      posts.push({ id: doc.id, ...doc.data() });
    });
    callback(posts);
  });
  
  return unsubscribe;
}

// Usage with React useEffect cleanup
// useEffect(() => {
//   const unsubscribe = subscribeToUserPosts(userId, setPosts);
//   return () => unsubscribe();
// }, [userId]);

Batch Writes and Transactions

Firestore supports atomic batch operations and transactions for data consistency. Batches execute multiple writes atomically but don't require reading documents. Transactions allow reading documents before writing, with automatic retry on contention.

// Web SDK — Batch writes
import { writeBatch, doc, collection } from 'firebase/firestore';

async function bulkUpdatePostStatuses(postIds, newStatus) {
  const batch = writeBatch(db);
  
  postIds.forEach((postId) => {
    const docRef = doc(db, 'posts', postId);
    batch.update(docRef, {
      status: newStatus,
      updatedAt: new Date()
    });
  });
  
  // A batch can handle up to 500 operations
  await batch.commit();
  console.log(`Batch updated ${postIds.length} posts`);
}

// Batch with mixed operations (create, update, delete)
async function createUserWithDefaultPosts(userId, userData) {
  const batch = writeBatch(db);
  
  // Create user document
  const userRef = doc(db, 'users', userId);
  batch.set(userRef, {
    ...userData,
    createdAt: new Date()
  });
  
  // Create welcome post
  const postRef = doc(collection(db, 'users', userId, 'posts'));
  batch.set(postRef, {
    title: 'Welcome!',
    content: 'Thanks for joining.',
    createdAt: new Date()
  });
  
  await batch.commit();
}
// Web SDK — Transactions
import { runTransaction } from 'firebase/firestore';

// Transaction for atomic read-modify-write
async function transferLike(fromPostId, toPostId, amount) {
  await runTransaction(db, async (transaction) => {
    // Read both documents within the transaction
    const fromDocRef = doc(db, 'posts', fromPostId);
    const toDocRef = doc(db, 'posts', toPostId);
    
    const fromDoc = await transaction.get(fromDocRef);
    const toDoc = await transaction.get(toDocRef);
    
    if (!fromDoc.exists() || !toDoc.exists()) {
      throw new Error('One or both posts do not exist');
    }
    
    const fromLikes = fromDoc.data().likes || 0;
    const toLikes = toDoc.data().likes || 0;
    
    if (fromLikes < amount) {
      throw new Error('Insufficient likes to transfer');
    }
    
    // Write updates atomically
    transaction.update(fromDocRef, { likes: fromLikes - amount });
    transaction.update(toDocRef, { likes: toLikes + amount });
  });
  
  console.log('Like transfer completed successfully');
}

// Transaction with custom retry logic
async function reserveUsername(username, userId) {
  try {
    await runTransaction(db, async (transaction) => {
      const usernameRef = doc(db, 'usernames', username.toLowerCase());
      const usernameDoc = await transaction.get(usernameRef);
      
      if (usernameDoc.exists() && usernameDoc.data().userId !== userId) {
        throw new Error('Username already taken');
      }
      
      transaction.set(usernameRef, {
        userId: userId,
        claimedAt: new Date()
      });
    });
    return true;
  } catch (error) {
    console.error('Username reservation failed:', error);
    return false;
  }
}

Working with Subcollections

// Web SDK — Subcollection operations
import { collection, doc, addDoc, getDocs, query, orderBy } from 'firebase/firestore';

// Add a document to a subcollection
async function addCommentToPost(postId, userId, text) {
  const commentsRef = collection(db, 'posts', postId, 'comments');
  
  await addDoc(commentsRef, {
    userId: userId,
    text: text,
    createdAt: new Date(),
    edited: false
  });
}

// Query a subcollection
async function getPostComments(postId) {
  const commentsRef = collection(db, 'posts', postId, 'comments');
  const q = query(commentsRef, orderBy('createdAt', 'asc'));
  
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data()
  }));
}

// Collection group query — search across all subcollections with the same name
import { collectionGroup } from 'firebase/firestore';

async function findUserComments(userId) {
  // Searches all 'comments' collections regardless of parent document
  const q = query(
    collectionGroup(db, 'comments'),
    where('userId', '==', userId),
    orderBy('createdAt', 'desc')
  );
  
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => ({
    id: doc.id,
    parentPath: doc.ref.parent.parent?.id, // The parent document ID
    ...doc.data()
  }));
}

Firestore Emulator for Local Development

The Firestore emulator runs a local instance of Firestore for development and testing without touching production data or consuming cloud quotas.

# Install the emulator via Firebase CLI
firebase init emulators

# Select Firestore emulator
# Configure port (default: 8080)
# Configure UI port (default: 4000)

# Start emulators
firebase emulators:start

# Start emulators and import data from a seed file
firebase emulators:start --import=./seed-data

# Export emulator data for later use
firebase emulators:export ./seed-data
// Connect to emulator in your application code
import { connectFirestoreEmulator } from 'firebase/firestore';

// Before any Firestore operations
connectFirestoreEmulator(db, 'localhost', 8080);

// Verify connection
console.log('Connected to Firestore emulator');

// Admin SDK emulator connection
// Set environment variable before initializing
// FIRESTORE_EMULATOR_HOST=localhost:8080

Best Practices for Firestore

Data Structure Design

Design your Firestore data model around how data will be queried, not how it will be stored. Unlike SQL databases, Firestore does not support arbitrary joins or aggregations across collections without reading data. Structure data to optimize read patterns.

Query Optimization

Queries in Firestore are fast when backed by proper indexes but can become expensive if not designed carefully. Every query result requires at least one document read, charged regardless of whether you cache data locally.

Security Rules Design

Performance and Cost Management

Error Handling Patterns

// Robust error handling wrapper
async function safeFirestoreOperation(operation) {
  try {
    return await operation();
  } catch (error) {
    if (error.code === 'permission-denied') {
      console.error('User lacks permission for this operation');
      // Handle gracefully — show appropriate UI message
    } else if (error.code === 'unavailable') {
      console.error('Firestore service is currently unavailable');
      // Implement retry logic or queue operation for later
      await new Promise(resolve => setTimeout(resolve, 2000));
      return safeFirestoreOperation(operation); // Retry once
    } else if (error.code === 'not-found') {
      console.error('Referenced document does not exist');
      // Handle missing document case
    } else if (error.code === 'aborted') {
      console.error('Transaction was aborted, consider retrying');
    } else {
      console.error('Unexpected Firestore error:', error);
      throw error; // Re-throw for upstream handling
    }
  }
}

// Usage
async function getUserSafely(userId) {
  return safeFirestoreOperation(() => 
    getDoc(doc(db, 'users', userId))
  );
}

Testing Firestore

Testing Firestore interactions requires either the emulator or mocking. The emulator provides the most accurate test environment because it runs the actual security rules and index logic.

// Example test setup with emulator (using Jest)
const { initializeTestEnvironment } = require('@firebase/rules-unit-testing');
const { doc, setDoc, getDoc } = require('firebase/firestore');

let testEnv;

beforeAll(async () => {
  testEnv = await initializeTestEnvironment({
    projectId: 'test-project-' + Date.now(),
    firestore: {
      rules: fs.readFileSync('firestore.rules', 'utf8'),
      host: 'localhost',
      port: 8080
    }
  });
});

afterAll(async () => {
  await testEnv.cleanup();
});

it('should allow authenticated user to read their own document', async () => {
  const alice = testEnv.authenticatedUser('alice-uid');
  const db = alice.firestore();
  
  // Seed data
  await testEnv.withSecurityRulesDisabled(async (adminDb) => {
    await setDoc(doc(adminDb, 'users', 'alice-uid'), {
      displayName: 'Alice',
      email: 'alice@example.com'
    });
  });
  
  // Test read
  const docSnap = await getDoc(doc(db, 'users', 'alice-uid'));
  expect(docSnap.exists()).toBe(true);
});

Conclusion

Firestore provides a complete, serverless database solution that eliminates infrastructure management while delivering real-time synchronization, offline persistence, and robust security controls. By following the setup steps outlined above — creating a project, enabling Firestore, integrating the SDK, configuring security rules, and defining indexes — you establish a solid foundation for any application. The practical patterns for CRUD operations, real-time listeners, transactions, and subcollections give you the tools to build sophisticated data layers. Adhering to best practices around data structure design, query optimization, security rules, and error handling ensures your application remains performant, secure, and cost-effective as it scales. Start with the emulator for local development, write comprehensive tests against your security rules, and monitor usage through the Firebase Console to maintain a healthy, well-functioning Firestore implementation.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles