firebase-firestore by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill firebase-firestore状态:生产就绪 最后更新:2026-01-25 依赖项:无(独立技能) 最新版本:firebase@12.8.0, firebase-admin@13.6.0
# Client SDK (web/mobile)
npm install firebase
# Admin SDK (server/backend)
npm install firebase-admin
// 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);
// 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();
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
关键点:
FIREBASE_PRIVATE_KEYimport {
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'));
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();
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();
}, []);
关键点:
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'])
);
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() }));
}
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 部署。
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();
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) });
});
}
关键点:
// 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;
}
// Users 集合
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 集合及其子集合
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 子集合
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;
}
}
}
# 部署规则
firebase deploy --only firestore:rules
# 部署规则和索引
firebase deploy --only firestore
{
"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": []
}
# 部署索引
firebase deploy --only firestore:indexes
关键点:
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');
}
});
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');
}
});
// 不要连接 users 和 posts...
// 直接在帖子文档中存储作者信息
// posts/{postId}
{
title: 'My Post',
content: '...',
authorId: 'user-123',
// 反规范化的作者数据,用于快速读取
author: {
name: 'John Doe',
avatarUrl: 'https://...'
},
createdAt: Timestamp
}
// 子集合:适用于父子关系
// posts/{postId}/comments/{commentId}
// - 易于查询帖子的所有评论
// - 删除帖子不会自动删除评论(使用 Cloud Functions)
// 根集合:适用于跨切面查询
// comments(带有 postId 字段)
// - 易于查询用户在所有帖子中的所有评论
// - 需要手动维护数据一致性
// 直接递增(低流量)
await updateDoc(doc(db, 'posts', postId), {
viewCount: increment(1)
});
// 分布式计数器(高流量 - 1000+ 写入/秒)
// 使用 Cloud Functions 聚合分片计数
// counters/{counterId}/shards/{shardId}
| 错误 | 原因 | 解决方案 |
|---|---|---|
permission-denied | 安全规则阻止访问 | 检查规则,确保用户已认证 |
not-found | 文档不存在 | 在访问数据前使用 exists() 检查 |
already-exists | 具有该 ID 的文档已存在 | 使用带合并的 setDoc 或生成新 ID |
resource-exhausted | 配额超出 | 升级计划或优化查询 |
failed-precondition | 查询缺少索引 | 创建复合索引(错误中包含链接) |
unavailable | 服务暂时不可用 | 实现带退避的重试机制 |
invalid-argument | 无效的查询组合 | 检查查询约束(见下文) |
deadline-exceeded | 操作超时 | 减少数据大小或分页 |
// 无效:在不同字段上使用多个不等式过滤器
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')
);
此技能可预防 10 个已记录的 Firestore 错误:
| 问题编号 | 错误/问题 | 描述 | 如何避免 | 来源 |
|---|---|---|---|---|
| #1 | permission-denied | 安全规则阻止操作 | 先在 Firebase 控制台模拟器中测试规则 | 常见 |
| #2 | failed-precondition(索引) | 缺少复合索引 | 点击错误链接创建索引,或在 firestore.indexes.json 中定义 | 常见 |
| #3 | 无效的查询组合 | 多个不等式过滤器 | 仅在一个字段上使用不等式,其他字段使用相等 | 文档 |
| #4 | 监听器导致的内存泄漏 | 未取消订阅 onSnapshot | 始终在清理时调用 unsubscribe(useEffect 返回) | 常见 |
| #5 | 离线持久化冲突 | 多个标签页同时启用持久化 | 使用 persistentMultipleTabManager() 或处理错误 | 文档 |
| #6 | 事务副作用 | 副作用运行多次 | 切勿在 runTransaction 内部执行副作用 | 文档 |
| #7 | 超出批量限制 | 超过 500 个操作 | 拆分为多个批次 | 文档 |
| #8 | resource-exhausted | 达到配额限制 | 实现分页,减少读取,使用缓存 | 常见 |
| #9 | 私钥换行符问题 | 环境变量中 \\n 未转换 | 在私钥上使用 .replace(/\\n/g, '\n') | 常见 |
| #10 | 集合组查询失败 | 缺少集合组索引 | 使用 queryScope: COLLECTION_GROUP 创建索引 | 文档 |
# 初始化 Firestore
firebase init firestore
# 启动模拟器
firebase emulators:start --only firestore
# 部署规则和索引
firebase deploy --only firestore
# 导出数据(用于备份)
gcloud firestore export gs://your-bucket/backups/$(date +%Y%m%d)
# 导入数据
gcloud firestore import gs://your-bucket/backups/20250125
{
"dependencies": {
"firebase": "^12.8.0"
},
"devDependencies": {
"firebase-admin": "^13.6.0"
}
}
最后验证 : 2026-01-25 | 技能版本 : 1.0.0
每周安装数
426
代码仓库
GitHub 星标数
650
首次出现
Jan 26, 2026
安全审计
安装于
claude-code324
opencode299
gemini-cli298
codex261
antigravity256
cursor240
Status : Production Ready Last Updated : 2026-01-25 Dependencies : None (standalone skill) Latest Versions : firebase@12.8.0, firebase-admin@13.6.0
# Client SDK (web/mobile)
npm install firebase
# Admin SDK (server/backend)
npm install firebase-admin
// 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);
// 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:
FIREBASE_PRIVATE_KEY in client codeimport {
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'));
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();
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:
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'])
);
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() }));
}
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.
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();
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:
// 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;
}
}
}
# Deploy rules
firebase deploy --only firestore:rules
# Deploy rules and indexes
firebase deploy --only firestore
{
"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:
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');
}
});
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');
}
});
// 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: 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
// 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 | 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 |
// 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')
);
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 |
# 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
{
"dependencies": {
"firebase": "^12.8.0"
},
"devDependencies": {
"firebase-admin": "^13.6.0"
}
}
Last verified : 2026-01-25 | Skill version : 1.0.0
Weekly Installs
426
Repository
GitHub Stars
650
First Seen
Jan 26, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code324
opencode299
gemini-cli298
codex261
antigravity256
cursor240
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
103,800 周安装
| 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 |
| #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 |