firebase-firestore

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Firebase Firestore Database

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

状态: 已就绪可用于生产环境 最后更新: 2026-01-25 依赖项: 无(独立技能) 最新版本: firebase@12.8.0, firebase-admin@13.6.0

Quick Start (5 Minutes)

快速入门(5分钟)

1. Install Firebase SDK

1. 安装Firebase SDK

bash
undefined
bash
undefined

Client SDK (web/mobile)

客户端SDK(网页/移动端)

npm install firebase
npm install firebase

Admin SDK (server/backend)

管理端SDK(服务器/后端)

npm install firebase-admin
undefined
npm install firebase-admin
undefined

2. Initialize Firebase (Client)

2. 初始化Firebase(客户端)

typescript
// 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);
typescript
// 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);

3. Initialize Firebase Admin (Server)

3. 初始化Firebase Admin(服务器)

typescript
// 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

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

// 仅初始化一次
if (!getApps().length) {
  initializeApp({
    credential: cert({
      projectId: process.env.FIREBASE_PROJECT_ID,
      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
      // 替换私钥中的转义换行符
      privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
    }),
  });
}

export const adminDb = getFirestore();
关键注意事项:
  • 绝不要在客户端代码中暴露
    FIREBASE_PRIVATE_KEY
  • 服务器端操作使用Admin SDK(绕过安全规则)
  • 已认证用户的操作使用客户端SDK

Core Operations

核心操作

Document CRUD (Client SDK - Modular v9+)

文档CRUD(客户端SDK - 模块化v9+)

typescript
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'));
typescript
import {
  collection,
  doc,
  addDoc,
  getDoc,
  getDocs,
  setDoc,
  updateDoc,
  deleteDoc,
  query,
  where,
  orderBy,
  limit,
  serverTimestamp,
  Timestamp,
} from 'firebase/firestore';
import { db } from './firebase';

// 创建 - 自动生成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);

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

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

// 读取 - 带查询条件的集合
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());
});

// 更新 - 合并字段(不会覆盖整个文档)
await updateDoc(doc(db, 'users', 'user-123'), {
  name: 'Jane Smith',
  updatedAt: serverTimestamp(),
});

// 更新 - 合并设置(不存在则创建)
await setDoc(doc(db, 'users', 'user-123'), {
  lastLogin: serverTimestamp(),
}, { merge: true });

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

Document CRUD (Admin SDK)

文档CRUD(Admin SDK)

typescript
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();

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

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

// 创建并指定ID
await adminDb.collection('users').doc('user-123').set({
  name: 'Jane Doe',
  createdAt: FieldValue.serverTimestamp(),
});

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

// 带查询条件的读取
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());
});

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

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

Real-Time Listeners (Client SDK)

实时监听器(客户端SDK)

typescript
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

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

// 监听单个文档
const unsubscribe = onSnapshot(doc(db, 'users', 'user-123'), (doc) => {
  if (doc.exists()) {
    console.log('Current data:', doc.data());
  }
});

// 监听带查询条件的集合
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);
  });
  // 更新UI显示消息
  setMessages(messages);
});

// 处理错误
const unsubscribeWithError = onSnapshot(
  doc(db, 'users', 'user-123'),
  (doc) => {
    // 处理更新
  },
  (error) => {
    console.error('Listener error:', error);
    // 处理权限拒绝等错误
  }
);

// 重要提示:使用完毕后取消订阅(React useEffect清理函数中)
useEffect(() => {
  const unsubscribe = onSnapshot(/* ... */);
  return () => unsubscribe();
}, []);
关键注意事项:
  • 必须取消监听器订阅以避免内存泄漏
  • 监听器会占用并发连接配额
  • 每个监听器对应一个WebSocket连接

Query Patterns

查询模式

Compound Queries

复合查询

typescript
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'])
);
typescript
import { query, where, orderBy, limit, startAfter, collection } from 'firebase/firestore';

// 多个where子句(需要复合索引)
const q = query(
  collection(db, 'products'),
  where('category', '==', 'electronics'),
  where('price', '<=', 1000),
  orderBy('price', 'asc')
);

// 范围查询(仅一个字段可使用不等式)
const rangeQuery = query(
  collection(db, 'events'),
  where('date', '>=', new Date('2025-01-01')),
  where('date', '<=', new Date('2025-12-31')),
  orderBy('date', 'asc')
);

// 数组包含
const arrayQuery = query(
  collection(db, 'posts'),
  where('tags', 'array-contains', 'firebase')
);

// 数组包含任意值(最多30个值)
const arrayAnyQuery = query(
  collection(db, 'posts'),
  where('tags', 'array-contains-any', ['firebase', 'google', 'cloud'])
);

// In查询(最多30个值)
const inQuery = query(
  collection(db, 'users'),
  where('status', 'in', ['active', 'pending'])
);

// Not in查询(最多10个值)
const notInQuery = query(
  collection(db, 'users'),
  where('status', 'not-in', ['banned', 'deleted'])
);

Pagination with Cursors

基于游标的分页

typescript
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() }));
}
typescript
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);

  // 保存最后一个文档用于下一页查询
  lastVisible = snapshot.docs[snapshot.docs.length - 1] || null;

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

Collection Group Queries

集合组查询

typescript
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
.

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

// 查询所有名为'comments'的子集合
// 结构:posts/{postId}/comments/{commentId}
const q = query(
  collectionGroup(db, 'comments'),
  where('authorId', '==', 'user-123')
);

const snapshot = await getDocs(q);
// 返回用户user-123在所有帖子下的评论
关键注意事项: 集合组查询需要索引。在Firebase控制台创建或通过
firestore.indexes.json
部署。

Batch Operations & Transactions

批量操作与事务

Batch Writes (Up to 500 operations)

批量写入(最多500个操作)

typescript
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();
typescript
import { writeBatch, doc, collection, serverTimestamp } from 'firebase/firestore';
import { db } from './firebase';

const batch = writeBatch(db);

// 添加多个文档
const usersRef = collection(db, 'users');
batch.set(doc(usersRef), { name: 'User 1', createdAt: serverTimestamp() });
batch.set(doc(usersRef), { name: 'User 2', createdAt: serverTimestamp() });

// 更新现有文档
batch.update(doc(db, 'counters', 'users'), { total: 100 });

// 删除文档
batch.delete(doc(db, 'temp', 'old-doc'));

// 原子提交所有操作
await batch.commit();

Transactions (Read then Write)

事务(先读后写)

typescript
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 < 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

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

// 用户间积分转账
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 < amount) {
      throw new Error('Insufficient credits');
    }

    transaction.update(fromRef, { credits: fromCredits - amount });
    transaction.update(toRef, { credits: increment(amount) });
  });
}
关键注意事项:
  • 事务可能失败并自动重试(最多5次)
  • 不要在事务内执行副作用(可能多次运行)
  • 事务中所有读取操作必须在写入之前

Security Rules

安全规则

Basic Rules Structure

基础规则结构

javascript
// 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'])
        && request.resource.data.name is string
        && request.resource.data.email is string;
    }

    // Users collection
    match /users/{userId} {
      allow read: if isAuthenticated();
      allow create: if isAuthenticated() && isOwner(userId) && 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() && 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;
    }
  }
}
javascript
// firestore.rules
rules_version = '2';

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

    // 辅助函数
    function isAuthenticated() {
      return request.auth != null;
    }

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

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

    // 用户集合
    match /users/{userId} {
      allow read: if isAuthenticated();
      allow create: if isAuthenticated() && isOwner(userId) && isValidUser();
      allow update: if isOwner(userId);
      allow delete: if isOwner(userId);
    }

    // 带自集合的帖子集合
    match /posts/{postId} {
      allow read: if resource.data.published == true || isOwner(resource.data.authorId);
      allow create: if isAuthenticated() && request.resource.data.authorId == request.auth.uid;
      allow update, delete: if isOwner(resource.data.authorId);

      // 评论子集合
      match /comments/{commentId} {
        allow read: if true;
        allow create: if isAuthenticated();
        allow update, delete: if isOwner(resource.data.authorId);
      }
    }

    // 仅管理员可访问的集合
    match /admin/{document=**} {
      allow read, write: if request.auth.token.admin == true;
    }
  }
}

Deploy Rules

部署规则

bash
undefined
bash
undefined

Deploy rules

部署规则

firebase deploy --only firestore:rules
firebase deploy --only firestore:rules

Deploy rules and indexes

部署规则和索引

firebase deploy --only firestore

---
firebase deploy --only firestore

---

Indexes

索引

Composite Indexes (firestore.indexes.json)

复合索引(firestore.indexes.json)

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": []
}
bash
undefined
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": []
}
bash
undefined

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

---
firebase deploy --only firestore:indexes

**关键注意事项:**
- Firestore会自动创建单字段索引
- 复合索引必须手动创建或通过错误链接创建
- 集合组查询始终需要索引

---

Offline Persistence

离线持久化

Enable Offline Support (Web)

启用离线支持(网页端)

typescript
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');
  }
});
typescript
import { initializeFirestore, persistentLocalCache, persistentMultipleTabManager } from 'firebase/firestore';
import { app } from './firebase';

// 启用多标签页离线持久化
const db = initializeFirestore(app, {
  localCache: persistentLocalCache({
    tabManager: persistentMultipleTabManager()
  })
});

// 或者:启用单标签页持久化(更简单)
import { enableIndexedDbPersistence, getFirestore } from 'firebase/firestore';

const db = getFirestore(app);
enableIndexedDbPersistence(db).catch((err) => {
  if (err.code === 'failed-precondition') {
    // 打开了多个标签页,只能在一个标签页中启用持久化
    console.warn('Persistence failed: multiple tabs open');
  } else if (err.code === 'unimplemented') {
    // 浏览器不支持持久化
    console.warn('Persistence not supported');
  }
});

Handle Offline State

处理离线状态

typescript
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');
  }
});

typescript
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

为读取性能反规范化

typescript
// 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
}
typescript
// 不要关联用户和帖子...
// 直接在帖子文档中存储作者信息

// posts/{postId}
{
  title: 'My Post',
  content: '...',
  authorId: 'user-123',
  // 反规范化的作者数据用于快速读取
  author: {
    name: 'John Doe',
    avatarUrl: 'https://...'
  },
  createdAt: Timestamp
}

Subcollections vs Root Collections

子集合 vs 根集合

typescript
// 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
typescript
// 子集合:适用于父子关系
// posts/{postId}/comments/{commentId}
// - 易于查询某个帖子的所有评论
// - 删除帖子不会自动删除评论(使用Cloud Functions)

// 根集合:适用于跨领域查询
// comments(包含postId字段)
// - 易于查询某个用户在所有帖子下的评论
// - 需要手动维护数据一致性

Counter Pattern (High-Write Scenarios)

计数器模式(高写入场景)

typescript
// 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}

typescript
// 直接递增(低流量)
await updateDoc(doc(db, 'posts', postId), {
  viewCount: increment(1)
});

// 分布式计数器(高流量 - 1000+次写入/秒)
// 使用Cloud Functions聚合分片计数
// counters/{counterId}/shards/{shardId}

Error Handling

错误处理

Common Errors and Solutions

常见错误及解决方案

ErrorCauseSolution
permission-denied
Security rules blocking accessCheck rules, ensure user authenticated
not-found
Document doesn't existUse
exists()
check before accessing data
already-exists
Document with ID already existsUse
setDoc
with merge or generate new ID
resource-exhausted
Quota exceededUpgrade plan or optimize queries
failed-precondition
Index missing for queryCreate composite index (link in error)
unavailable
Service temporarily unavailableImplement retry with backoff
invalid-argument
Invalid query combinationCheck query constraints (see below)
deadline-exceeded
Operation timeoutReduce data size or paginate
错误原因解决方案
permission-denied
安全规则阻止访问检查规则,确保用户已认证
not-found
文档不存在访问数据前使用
exists()
检查
already-exists
该ID的文档已存在使用
setDoc
并开启merge或生成新ID
resource-exhausted
配额超限升级套餐或优化查询
failed-precondition
查询缺少索引创建复合索引(错误信息中的链接)
unavailable
服务暂时不可用实现带退避策略的重试机制
invalid-argument
查询组合无效检查查询约束(见下文)
deadline-exceeded
操作超时减少数据量或使用分页

Query Constraints

查询约束

typescript
// 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')
);

typescript
// 无效:在不同字段上使用多个不等式过滤器
query(collection(db, 'posts'),
  where('date', '>', startDate),
  where('likes', '>', 100)  // 错误:不能在第二个字段上使用不等式
);

// 有效:在一个字段上使用范围,其他字段使用相等
query(collection(db, 'posts'),
  where('category', '==', 'tech'),
  where('date', '>', startDate)
);

// 无效:orderBy字段与不等式字段不同
query(collection(db, 'posts'),
  where('date', '>', startDate),
  orderBy('likes')  // 错误:必须先orderBy('date')
);

// 有效:先按不等式字段排序
query(collection(db, 'posts'),
  where('date', '>', startDate),
  orderBy('date'),
  orderBy('likes')
);

Known Issues Prevention

已知问题预防

This skill prevents 10 documented Firestore errors:
Issue #Error/IssueDescriptionHow to AvoidSource
#1
permission-denied
Security rules blocking operationTest rules in Firebase Console emulator firstCommon
#2
failed-precondition
(index)
Composite index missingClick error link to create index, or define in firestore.indexes.jsonCommon
#3Invalid query combinationMultiple inequality filtersUse inequality on one field only, equality on othersDocs
#4Memory leak from listenersNot unsubscribing from onSnapshotAlways call unsubscribe in cleanup (useEffect return)Common
#5Offline persistence conflictMultiple tabs with persistenceUse
persistentMultipleTabManager()
or handle error
Docs
#6Transaction side effectsSide effects run multiple timesNever perform side effects inside runTransactionDocs
#7Batch limit exceededMore than 500 operationsSplit into multiple batchesDocs
#8
resource-exhausted
Quota limits hitImplement pagination, reduce reads, use cachingCommon
#9Private key newline issue
\\n
not converted in env var
Use
.replace(/\\n/g, '\n')
on private key
Common
#10Collection group query failsMissing collection group indexCreate index with
queryScope: COLLECTION_GROUP
Docs

本技能可预防10种已记录的Firestore错误:
问题编号错误/问题描述避免方法来源
#1
permission-denied
安全规则阻止操作先在Firebase控制台模拟器中测试规则常见问题
#2
failed-precondition
(索引)
缺少复合索引点击错误链接创建索引,或在firestore.indexes.json中定义常见问题
#3查询组合无效多个不等式过滤器仅在一个字段上使用不等式,其他字段使用相等文档
#4监听器导致内存泄漏未取消onSnapshot订阅始终在清理函数中调用unsubscribe(如React useEffect返回)常见问题
#5离线持久化冲突多个标签页开启持久化使用
persistentMultipleTabManager()
或处理错误
文档
#6事务副作用副作用多次运行绝不要在runTransaction内执行副作用文档
#7批量操作超出限制超过500个操作拆分为多个批量操作文档
#8
resource-exhausted
达到配额限制实现分页,减少读取次数,使用缓存常见问题
#9私钥换行符问题环境变量中的
\\n
未转换
对私钥使用
.replace(/\\n/g, '\n')
常见问题
#10集合组查询失败缺少集合组索引创建
queryScope: COLLECTION_GROUP
的索引
文档

Firebase CLI Commands

Firebase CLI命令

bash
undefined
bash
undefined

Initialize Firestore

初始化Firestore

firebase init firestore
firebase init firestore

Start emulators

启动模拟器

firebase emulators:start --only firestore
firebase emulators:start --only firestore

Deploy rules and indexes

部署规则和索引

firebase deploy --only firestore
firebase deploy --only firestore

Export data (for backup)

导出数据(备份)

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

Import data

导入数据

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

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

---

Package Versions (Verified 2026-01-25)

包版本(2026-01-25已验证)

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

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

Official Documentation

官方文档