firebase-firestore

Firebase Firestore Database

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "firebase-firestore" with this command: npx skills add jezweb/claude-skills/jezweb-claude-skills-firebase-firestore

Firebase Firestore Database

Status: Production Ready Last Updated: 2026-01-25 Dependencies: None (standalone skill) Latest Versions: firebase@12.8.0, firebase-admin@13.6.0

Quick Start (5 Minutes)

  1. Install Firebase SDK

Client SDK (web/mobile)

npm install firebase

Admin SDK (server/backend)

npm install firebase-admin

  1. Initialize Firebase (Client)

// src/lib/firebase.ts import { initializeApp } from 'firebase/app'; import { getFirestore } from 'firebase/firestore';

const firebaseConfig = { apiKey: process.env.FIREBASE_API_KEY, authDomain: process.env.FIREBASE_AUTH_DOMAIN, projectId: process.env.FIREBASE_PROJECT_ID, storageBucket: process.env.FIREBASE_STORAGE_BUCKET, messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, appId: process.env.FIREBASE_APP_ID, };

const app = initializeApp(firebaseConfig); export const db = getFirestore(app);

  1. Initialize Firebase Admin (Server)

// src/lib/firebase-admin.ts import { initializeApp, cert, getApps } from 'firebase-admin/app'; import { getFirestore } from 'firebase-admin/firestore';

// Initialize only once if (!getApps().length) { initializeApp({ credential: cert({ projectId: process.env.FIREBASE_PROJECT_ID, clientEmail: process.env.FIREBASE_CLIENT_EMAIL, // Replace escaped newlines in private key privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\n/g, '\n'), }), }); }

export const adminDb = getFirestore();

CRITICAL:

  • Never expose FIREBASE_PRIVATE_KEY in client code

  • Use Admin SDK for server-side operations (bypasses security rules)

  • Use Client SDK for authenticated user operations

Core Operations

Document CRUD (Client SDK - Modular v9+)

import { collection, doc, addDoc, getDoc, getDocs, setDoc, updateDoc, deleteDoc, query, where, orderBy, limit, serverTimestamp, Timestamp, } from 'firebase/firestore'; import { db } from './firebase';

// CREATE - Auto-generated ID const docRef = await addDoc(collection(db, 'users'), { name: 'John Doe', email: 'john@example.com', createdAt: serverTimestamp(), }); console.log('Created document with ID:', docRef.id);

// CREATE - Specific ID await setDoc(doc(db, 'users', 'user-123'), { name: 'Jane Doe', email: 'jane@example.com', createdAt: serverTimestamp(), });

// READ - Single document const docSnap = await getDoc(doc(db, 'users', 'user-123')); if (docSnap.exists()) { console.log('Document data:', docSnap.data()); } else { console.log('No such document!'); }

// READ - Collection with query const q = query( collection(db, 'users'), where('email', '==', 'john@example.com'), orderBy('createdAt', 'desc'), limit(10) ); const querySnapshot = await getDocs(q); querySnapshot.forEach((doc) => { console.log(doc.id, ' => ', doc.data()); });

// UPDATE - Merge fields (doesn't overwrite entire document) await updateDoc(doc(db, 'users', 'user-123'), { name: 'Jane Smith', updatedAt: serverTimestamp(), });

// UPDATE - Set with merge (creates if doesn't exist) await setDoc(doc(db, 'users', 'user-123'), { lastLogin: serverTimestamp(), }, { merge: true });

// DELETE await deleteDoc(doc(db, 'users', 'user-123'));

Document CRUD (Admin SDK)

import { adminDb } from './firebase-admin'; import { FieldValue, Timestamp } from 'firebase-admin/firestore';

// CREATE const docRef = await adminDb.collection('users').add({ name: 'John Doe', createdAt: FieldValue.serverTimestamp(), });

// CREATE with specific ID await adminDb.collection('users').doc('user-123').set({ name: 'Jane Doe', createdAt: FieldValue.serverTimestamp(), });

// READ const doc = await adminDb.collection('users').doc('user-123').get(); if (doc.exists) { console.log('Document data:', doc.data()); }

// READ with query const snapshot = await adminDb .collection('users') .where('email', '==', 'john@example.com') .orderBy('createdAt', 'desc') .limit(10) .get();

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

// UPDATE await adminDb.collection('users').doc('user-123').update({ name: 'Jane Smith', updatedAt: FieldValue.serverTimestamp(), });

// DELETE await adminDb.collection('users').doc('user-123').delete();

Real-Time Listeners (Client SDK)

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

// Listen to single document const unsubscribe = onSnapshot(doc(db, 'users', 'user-123'), (doc) => { if (doc.exists()) { console.log('Current data:', doc.data()); } });

// Listen to collection with query const q = query( collection(db, 'messages'), where('roomId', '==', 'room-123'), orderBy('createdAt', 'desc'), limit(50) );

const unsubscribeMessages = onSnapshot(q, (querySnapshot) => { const messages: Message[] = []; querySnapshot.forEach((doc) => { messages.push({ id: doc.id, ...doc.data() } as Message); }); // Update UI with messages setMessages(messages); });

// Handle errors const unsubscribeWithError = onSnapshot( doc(db, 'users', 'user-123'), (doc) => { // Handle updates }, (error) => { console.error('Listener error:', error); // Handle permission denied, etc. } );

// IMPORTANT: Unsubscribe when done (React useEffect cleanup) useEffect(() => { const unsubscribe = onSnapshot(/* ... */); return () => unsubscribe(); }, []);

CRITICAL:

  • Always unsubscribe from listeners to prevent memory leaks

  • Listeners count against your concurrent connection limit

  • Each listener is a WebSocket connection

Query Patterns

Compound Queries

import { query, where, orderBy, limit, startAfter, collection } from 'firebase/firestore';

// Multiple where clauses (requires composite index) const q = query( collection(db, 'products'), where('category', '==', 'electronics'), where('price', '<=', 1000), orderBy('price', 'asc') );

// Range query (only one field can have inequality) const rangeQuery = query( collection(db, 'events'), where('date', '>=', new Date('2025-01-01')), where('date', '<=', new Date('2025-12-31')), orderBy('date', 'asc') );

// Array contains const arrayQuery = query( collection(db, 'posts'), where('tags', 'array-contains', 'firebase') );

// Array contains any (max 30 values) const arrayAnyQuery = query( collection(db, 'posts'), where('tags', 'array-contains-any', ['firebase', 'google', 'cloud']) );

// In query (max 30 values) const inQuery = query( collection(db, 'users'), where('status', 'in', ['active', 'pending']) );

// Not in query (max 10 values) const notInQuery = query( collection(db, 'users'), where('status', 'not-in', ['banned', 'deleted']) );

Pagination with Cursors

import { query, orderBy, limit, startAfter, getDocs, collection, DocumentSnapshot } from 'firebase/firestore';

let lastVisible: DocumentSnapshot | null = null;

async function getNextPage() { let q = query( collection(db, 'posts'), orderBy('createdAt', 'desc'), limit(10) );

if (lastVisible) { q = query(q, startAfter(lastVisible)); }

const snapshot = await getDocs(q);

// Save last document for next page lastVisible = snapshot.docs[snapshot.docs.length - 1] || null;

return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); }

Collection Group Queries

import { collectionGroup, query, where, getDocs } from 'firebase/firestore';

// Query across all subcollections named 'comments' // Structure: posts/{postId}/comments/{commentId} const q = query( collectionGroup(db, 'comments'), where('authorId', '==', 'user-123') );

const snapshot = await getDocs(q); // Returns all comments by user-123 across all posts

CRITICAL: Collection group queries require an index. Create in Firebase Console or deploy via firestore.indexes.json .

Batch Operations & Transactions

Batch Writes (Up to 500 operations)

import { writeBatch, doc, collection, serverTimestamp } from 'firebase/firestore'; import { db } from './firebase';

const batch = writeBatch(db);

// Add multiple documents const usersRef = collection(db, 'users'); batch.set(doc(usersRef), { name: 'User 1', createdAt: serverTimestamp() }); batch.set(doc(usersRef), { name: 'User 2', createdAt: serverTimestamp() });

// Update existing document batch.update(doc(db, 'counters', 'users'), { total: 100 });

// Delete document batch.delete(doc(db, 'temp', 'old-doc'));

// Commit all operations atomically await batch.commit();

Transactions (Read then Write)

import { runTransaction, doc, increment } from 'firebase/firestore'; import { db } from './firebase';

// Transfer credits between users async function transferCredits(fromId: string, toId: string, amount: number) { await runTransaction(db, async (transaction) => { const fromRef = doc(db, 'users', fromId); const toRef = doc(db, 'users', toId);

const fromDoc = await transaction.get(fromRef);
const toDoc = await transaction.get(toRef);

if (!fromDoc.exists() || !toDoc.exists()) {
  throw new Error('User not found');
}

const fromCredits = fromDoc.data().credits;
if (fromCredits &#x3C; amount) {
  throw new Error('Insufficient credits');
}

transaction.update(fromRef, { credits: fromCredits - amount });
transaction.update(toRef, { credits: increment(amount) });

}); }

CRITICAL:

  • Transactions can fail and retry automatically (up to 5 times)

  • Don't perform side effects inside transaction (may run multiple times)

  • All reads must come before writes in a transaction

Security Rules

Basic Rules Structure

// firestore.rules rules_version = '2';

service cloud.firestore { match /databases/{database}/documents {

// Helper functions
function isAuthenticated() {
  return request.auth != null;
}

function isOwner(userId) {
  return request.auth.uid == userId;
}

function isValidUser() {
  return request.resource.data.keys().hasAll(['name', 'email'])
    &#x26;&#x26; request.resource.data.name is string
    &#x26;&#x26; request.resource.data.email is string;
}

// Users collection
match /users/{userId} {
  allow read: if isAuthenticated();
  allow create: if isAuthenticated() &#x26;&#x26; isOwner(userId) &#x26;&#x26; isValidUser();
  allow update: if isOwner(userId);
  allow delete: if isOwner(userId);
}

// Posts collection with subcollections
match /posts/{postId} {
  allow read: if resource.data.published == true || isOwner(resource.data.authorId);
  allow create: if isAuthenticated() &#x26;&#x26; request.resource.data.authorId == request.auth.uid;
  allow update, delete: if isOwner(resource.data.authorId);

  // Comments subcollection
  match /comments/{commentId} {
    allow read: if true;
    allow create: if isAuthenticated();
    allow update, delete: if isOwner(resource.data.authorId);
  }
}

// Admin-only collection
match /admin/{document=**} {
  allow read, write: if request.auth.token.admin == true;
}

} }

Deploy Rules

Deploy rules

firebase deploy --only firestore:rules

Deploy rules and indexes

firebase deploy --only firestore

Indexes

Composite Indexes (firestore.indexes.json)

{ "indexes": [ { "collectionGroup": "products", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "category", "order": "ASCENDING" }, { "fieldPath": "price", "order": "ASCENDING" } ] }, { "collectionGroup": "comments", "queryScope": "COLLECTION_GROUP", "fields": [ { "fieldPath": "authorId", "order": "ASCENDING" }, { "fieldPath": "createdAt", "order": "DESCENDING" } ] } ], "fieldOverrides": [] }

Deploy indexes

firebase deploy --only firestore:indexes

CRITICAL:

  • Firestore auto-creates single-field indexes

  • Composite indexes must be created manually or via error link

  • Collection group queries always require an index

Offline Persistence

Enable Offline Support (Web)

import { initializeFirestore, persistentLocalCache, persistentMultipleTabManager } from 'firebase/firestore'; import { app } from './firebase';

// Enable multi-tab offline persistence const db = initializeFirestore(app, { localCache: persistentLocalCache({ tabManager: persistentMultipleTabManager() }) });

// OR: Enable single-tab persistence (simpler) import { enableIndexedDbPersistence, getFirestore } from 'firebase/firestore';

const db = getFirestore(app); enableIndexedDbPersistence(db).catch((err) => { if (err.code === 'failed-precondition') { // Multiple tabs open, persistence can only be enabled in one tab console.warn('Persistence failed: multiple tabs open'); } else if (err.code === 'unimplemented') { // Browser doesn't support persistence console.warn('Persistence not supported'); } });

Handle Offline State

import { onSnapshot, doc, SnapshotMetadata } from 'firebase/firestore';

onSnapshot(doc(db, 'users', 'user-123'), (doc) => { const source = doc.metadata.fromCache ? 'local cache' : 'server'; console.log(Data came from ${source});

if (doc.metadata.hasPendingWrites) { console.log('Local changes pending sync'); } });

Data Modeling Best Practices

Denormalization for Read Performance

// Instead of joining users and posts... // Store author info directly in post document

// posts/{postId} { title: 'My Post', content: '...', authorId: 'user-123', // Denormalized author data for fast reads author: { name: 'John Doe', avatarUrl: 'https://...' }, createdAt: Timestamp }

Subcollections vs Root Collections

// Subcollections: Good for parent-child relationships // posts/{postId}/comments/{commentId} // - Easy to query all comments for a post // - Deleting post doesn't auto-delete comments (use Cloud Functions)

// Root collections: Good for cross-cutting queries // comments (with postId field) // - Easy to query all comments by a user across posts // - Requires manual data consistency

Counter Pattern (High-Write Scenarios)

// Direct increment (low traffic) await updateDoc(doc(db, 'posts', postId), { viewCount: increment(1) });

// Distributed counter (high traffic - 1000+ writes/sec) // Use Cloud Functions to aggregate shard counts // counters/{counterId}/shards/{shardId}

Error Handling

Common Errors and Solutions

Error Cause Solution

permission-denied

Security rules blocking access Check rules, ensure user authenticated

not-found

Document doesn't exist Use exists() check before accessing data

already-exists

Document with ID already exists Use setDoc with merge or generate new ID

resource-exhausted

Quota exceeded Upgrade plan or optimize queries

failed-precondition

Index missing for query Create composite index (link in error)

unavailable

Service temporarily unavailable Implement retry with backoff

invalid-argument

Invalid query combination Check query constraints (see below)

deadline-exceeded

Operation timeout Reduce data size or paginate

Query Constraints

// INVALID: Multiple inequality filters on different fields query(collection(db, 'posts'), where('date', '>', startDate), where('likes', '>', 100) // ERROR: Can't use inequality on second field );

// VALID: Use range on one field, equality on others query(collection(db, 'posts'), where('category', '==', 'tech'), where('date', '>', startDate) );

// INVALID: orderBy field different from inequality field query(collection(db, 'posts'), where('date', '>', startDate), orderBy('likes') // ERROR: Must orderBy('date') first );

// VALID: orderBy inequality field first query(collection(db, 'posts'), where('date', '>', startDate), orderBy('date'), orderBy('likes') );

Known Issues Prevention

This skill prevents 10 documented Firestore errors:

Issue # Error/Issue Description How to Avoid Source

#1 permission-denied

Security rules blocking operation Test rules in Firebase Console emulator first Common

#2 failed-precondition (index) Composite index missing Click error link to create index, or define in firestore.indexes.json Common

#3 Invalid query combination Multiple inequality filters Use inequality on one field only, equality on others Docs

#4 Memory leak from listeners Not unsubscribing from onSnapshot Always call unsubscribe in cleanup (useEffect return) Common

#5 Offline persistence conflict Multiple tabs with persistence Use persistentMultipleTabManager() or handle error Docs

#6 Transaction side effects Side effects run multiple times Never perform side effects inside runTransaction Docs

#7 Batch limit exceeded More than 500 operations Split into multiple batches Docs

#8 resource-exhausted

Quota limits hit Implement pagination, reduce reads, use caching Common

#9 Private key newline issue \n not converted in env var Use .replace(/\n/g, '\n') on private key Common

#10 Collection group query fails Missing collection group index Create index with queryScope: COLLECTION_GROUP

Docs

Firebase CLI Commands

Initialize Firestore

firebase init firestore

Start emulators

firebase emulators:start --only firestore

Deploy rules and indexes

firebase deploy --only firestore

Export data (for backup)

gcloud firestore export gs://your-bucket/backups/$(date +%Y%m%d)

Import data

gcloud firestore import gs://your-bucket/backups/20250125

Package Versions (Verified 2026-01-25)

{ "dependencies": { "firebase": "^12.8.0" }, "devDependencies": { "firebase-admin": "^13.6.0" } }

Official Documentation

Last verified: 2026-01-25 | Skill version: 1.0.0

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tailwind-v4-shadcn

No summary provided by upstream source.

Repository SourceNeeds Review
2.7K-jezweb
General

tanstack-query

No summary provided by upstream source.

Repository SourceNeeds Review
2.5K-jezweb
General

fastapi

No summary provided by upstream source.

Repository SourceNeeds Review
General

zustand-state-management

No summary provided by upstream source.

Repository SourceNeeds Review
1.2K-jezweb