Use Cases (更新: 2026/6/1)

用 Claude Code 实战 Firebase 开发:认证、规则、函数与部署

用 Claude Code 开发 Firebase 应用:覆盖 Auth、Firestore 安全规则、Functions、Hosting、Emulator、环境隔离与成本安全。

用 Claude Code 实战 Firebase 开发:认证、规则、函数与部署

在让 Claude Code 写 Firebase 代码之前

Firebase 很适合小团队快速构建带登录、数据库、后端函数和静态托管的 Web 应用。常见组合是 Firebase Authentication、Cloud Firestore、Cloud Functions for Firebase、Firebase Hosting 和 Local Emulator Suite。BaaS 可以理解为“把常见后端能力作为服务使用”,这样前端团队也能较快交付完整功能。

但 Firebase 的风险也很明显:应用“能跑起来”不等于“可以安全上线”。Firestore Security Rules 写得太宽,用户可能读到别人的数据;Cloud Functions 里使用 Admin SDK 时会绕过 Security Rules,因此必须在函数内部重新做权限校验;开发项目和生产项目混用时,测试写入也可能变成真实数据和真实费用。

让 Claude Code 参与 Firebase 开发时,不要只说“做一个页面”。更好的粒度是一个纵向功能切片:例如“支持工单功能”要同时包含登录状态、写入 Firestore、Security Rules、规则测试、Cloud Functions 中的特权操作,以及 Hosting 配置。

相关基础可以继续阅读认证实现指南CI/CD 设置指南,如果想比较后端方案,也可以看Claude Code 与 Supabase 集成

flowchart LR
  A["User"] --> B["Firebase Authentication"]
  B --> C["React or Astro UI"]
  C --> D["Cloud Firestore"]
  D --> E["Security Rules"]
  D --> F["Cloud Functions v2"]
  C --> G["Firebase Hosting"]
  H["Emulator Suite"] --> B
  H --> D
  H --> F

本文使用 Vite + React + TypeScript 作为示例。Next.js 或 Astro 项目也可以复用同样思路,只需要调整环境变量和路由配置。

架构与官方资料

Firebase 开发要优先看官方文档,尤其是 Security Rules 和 Emulator Suite。网上旧文章里的代码可能还能运行,但权限模型或部署方式不一定适合当前项目。

领域官方链接适合交给 Claude Code 的任务
AuthenticationFirebase Authentication登录 UI、用户资料写入、登录状态管理
FirestoreCloud Firestore集合设计、查询函数、索引前提
Security RulesFirestore Security Rules规则、拒绝用例测试、所有者校验
Cloud FunctionsCloud Functions for Firebase服务端校验、通知、聚合、外部 API
HostingFirebase HostingSPA 发布、缓存、预览频道
EmulatorLocal Emulator Suite本地验证、规则覆盖、CI 测试
价格Firebase pricing读取次数、函数调用、日志量估算
Claude CodeClaude Code docs任务拆分、差异审查、测试执行

建议先让 Claude Code 读取并遵守类似下面的项目结构。

.
├─ firebase.json
├─ firestore.rules
├─ firestore.indexes.json
├─ .firebaserc
├─ functions/
│  ├─ package.json
│  └─ src/index.ts
└─ src/
   ├─ lib/firebase.ts
   ├─ lib/tickets.ts
   └─ lib/useAuth.tsx

先固定环境隔离

许多 Firebase 事故不是复杂 bug,而是项目指向错了。开发、预发、生产要在一开始就分清楚,Claude Code 也必须知道不能直接部署生产。

{
  "projects": {
    "dev": "claudecodelab-firebase-dev",
    "stg": "claudecodelab-firebase-stg",
    "prod": "claudecodelab-firebase-prod"
  }
}

firebase.json 统一管理 Firestore 规则、索引、Functions、Hosting 和本地模拟器。

{
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "functions": [
    {
      "source": "functions",
      "codebase": "default",
      "runtime": "nodejs20"
    }
  ],
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "headers": [
      {
        "source": "/assets/**",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "public, max-age=31536000, immutable"
          }
        ]
      }
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  },
  "emulators": {
    "auth": {
      "port": 9099
    },
    "functions": {
      "port": 5001
    },
    "firestore": {
      "port": 8080
    },
    "hosting": {
      "port": 5000
    },
    "ui": {
      "enabled": true,
      "port": 4000
    },
    "singleProjectMode": true
  }
}

Vite 项目的 .env.local 可以这样写。Firebase Web API key 不是服务端密钥,但服务账号 JSON 和 Admin SDK 凭据绝不能放进前端。

VITE_FIREBASE_API_KEY=replace-me
VITE_FIREBASE_AUTH_DOMAIN=claudecodelab-firebase-dev.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=claudecodelab-firebase-dev
VITE_FIREBASE_STORAGE_BUCKET=claudecodelab-firebase-dev.appspot.com
VITE_FIREBASE_APP_ID=replace-me
VITE_USE_FIREBASE_EMULATORS=true

Authentication 与客户端初始化

下面的 src/lib/firebase.ts 初始化 App、Auth、Firestore 和 Functions,并在本地开发时连接模拟器。

// src/lib/firebase.ts
import { initializeApp, getApp, getApps } from "firebase/app";
import {
  connectAuthEmulator,
  getAuth,
  GoogleAuthProvider,
} from "firebase/auth";
import { connectFirestoreEmulator, getFirestore } from "firebase/firestore";
import { connectFunctionsEmulator, getFunctions } from "firebase/functions";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
};

const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig);

export const auth = getAuth(app);
export const db = getFirestore(app);
export const functions = getFunctions(app, "asia-northeast1");
export const googleProvider = new GoogleAuthProvider();

const shouldUseEmulators =
  import.meta.env.DEV && import.meta.env.VITE_USE_FIREBASE_EMULATORS === "true";

const globalState = globalThis as typeof globalThis & {
  __firebaseEmulatorsConnected?: boolean;
};

if (shouldUseEmulators && !globalState.__firebaseEmulatorsConnected) {
  connectAuthEmulator(auth, "http://127.0.0.1:9099", {
    disableWarnings: true,
  });
  connectFirestoreEmulator(db, "127.0.0.1", 8080);
  connectFunctionsEmulator(functions, "127.0.0.1", 5001);
  globalState.__firebaseEmulatorsConnected = true;
}

登录状态建议封装成 Hook,并在首次登录时合并写入 users/{uid}

// src/lib/useAuth.tsx
import { useEffect, useState } from "react";
import {
  onAuthStateChanged,
  signInWithPopup,
  signOut,
  type User,
} from "firebase/auth";
import { doc, serverTimestamp, setDoc } from "firebase/firestore";
import { auth, db, googleProvider } from "./firebase";

type AuthState = {
  user: User | null;
  loading: boolean;
  signInWithGoogle: () => Promise<void>;
  logout: () => Promise<void>;
};

export function useAuth(): AuthState {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    return onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser);
      setLoading(false);
    });
  }, []);

  async function signInWithGoogle() {
    const result = await signInWithPopup(auth, googleProvider);
    await setDoc(
      doc(db, "users", result.user.uid),
      {
        uid: result.user.uid,
        email: result.user.email,
        displayName: result.user.displayName,
        photoURL: result.user.photoURL,
        updatedAt: serverTimestamp(),
      },
      { merge: true },
    );
  }

  return {
    user,
    loading,
    signInWithGoogle,
    logout: () => signOut(auth),
  };
}

可以这样向 Claude Code 下任务:

实现 Firebase Auth 的 Google 登录。
- 技术栈是 Vite + React + TypeScript
- 复用现有 src/lib/firebase.ts
- 首次登录时 merge 写入 users/{uid}
- 返回 logout 方法
- 不创建、不展示、不保存服务账号凭据
- 完成后说明类型检查、Firestore Rules 影响和手动确认项

Firestore 数据设计与 CRUD

示例功能是“支持工单”。它覆盖三个真实用例:会员门户中用户查看自己的工单;客服流程中通过 Cloud Functions 关闭工单;管理后台在新工单创建时生成通知和统计。

客户端只允许创建和读取当前用户自己的工单。不要先读取所有文档再在页面中筛选,这既浪费读取次数,也不符合安全规则。

// src/lib/tickets.ts
import {
  addDoc,
  collection,
  getDocs,
  limit,
  orderBy,
  query,
  serverTimestamp,
  where,
  type Timestamp,
} from "firebase/firestore";
import { db } from "./firebase";

export type TicketStatus = "open" | "closed";

export type Ticket = {
  id: string;
  userId: string;
  title: string;
  body: string;
  status: TicketStatus;
  createdAt: Timestamp;
  updatedAt: Timestamp;
};

type CreateTicketInput = {
  userId: string;
  title: string;
  body: string;
};

export async function createTicket(input: CreateTicketInput): Promise<string> {
  const title = input.title.trim();
  const body = input.body.trim();

  if (title.length === 0 || title.length > 120) {
    throw new Error("Title must be between 1 and 120 characters.");
  }

  if (body.length === 0 || body.length > 4000) {
    throw new Error("Body must be between 1 and 4000 characters.");
  }

  const docRef = await addDoc(collection(db, "tickets"), {
    userId: input.userId,
    title,
    body,
    status: "open",
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp(),
  });

  return docRef.id;
}

export async function listMyTickets(userId: string): Promise<Ticket[]> {
  const ticketsQuery = query(
    collection(db, "tickets"),
    where("userId", "==", userId),
    orderBy("createdAt", "desc"),
    limit(20),
  );

  const snapshot = await getDocs(ticketsQuery);

  return snapshot.docs.map((ticketDoc) => ({
    id: ticketDoc.id,
    ...(ticketDoc.data() as Omit<Ticket, "id">),
  }));
}

whereorderBy 组合可能需要复合索引。建议把索引也提交到代码库。

{
  "indexes": [
    {
      "collectionGroup": "tickets",
      "queryScope": "COLLECTION",
      "fields": [
        {
          "fieldPath": "userId",
          "order": "ASCENDING"
        },
        {
          "fieldPath": "createdAt",
          "order": "DESCENDING"
        }
      ]
    }
  ],
  "fieldOverrides": []
}

Firestore Security Rules 不是过滤器

Security Rules 不会把一个宽查询自动过滤成安全结果。查询本身必须只可能返回允许访问的文档。因此,getDocs(collection(db, "tickets")) 后再在 React 中筛选用户,是错误做法。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function signedIn() {
      return request.auth != null;
    }

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

    function ticketFieldsAreValid() {
      return request.resource.data.keys().hasOnly([
        "userId",
        "title",
        "body",
        "status",
        "createdAt",
        "updatedAt"
      ])
      && request.resource.data.title is string
      && request.resource.data.title.size() > 0
      && request.resource.data.title.size() <= 120
      && request.resource.data.body is string
      && request.resource.data.body.size() > 0
      && request.resource.data.body.size() <= 4000;
    }

    match /users/{userId} {
      allow create, read, update: if isOwner(userId);
      allow delete: if false;
    }

    match /tickets/{ticketId} {
      allow create: if signedIn()
        && request.resource.data.userId == request.auth.uid
        && request.resource.data.status == "open"
        && ticketFieldsAreValid();

      allow read: if signedIn()
        && resource.data.userId == request.auth.uid;

      allow update: if signedIn()
        && resource.data.userId == request.auth.uid
        && request.resource.data.userId == resource.data.userId
        && request.resource.data.status == resource.data.status
        && request.resource.data.diff(resource.data).affectedKeys()
          .hasOnly(["title", "body", "updatedAt"])
        && ticketFieldsAreValid();

      allow delete: if false;
    }

    match /adminStats/{docId} {
      allow read, write: if false;
    }
  }
}

审查重点是:request.auth.uid 是否和文档所有者比较;是否限制字段集合;是否明确禁止删除;管理端状态是否没有暴露给 Web 客户端。

用 Emulator Suite 测试规则

本地测试能把权限假设变成可重复的检查。先安装依赖:

npm install -D vitest @firebase/rules-unit-testing firebase
firebase setup:emulators:firestore
// tests/firestore.rules.test.ts
import { readFileSync } from "node:fs";
import {
  assertFails,
  assertSucceeds,
  initializeTestEnvironment,
  type RulesTestEnvironment,
} from "@firebase/rules-unit-testing";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { doc, getDoc, setDoc, updateDoc } from "firebase/firestore";

let testEnv: RulesTestEnvironment;

beforeAll(async () => {
  testEnv = await initializeTestEnvironment({
    projectId: "claudecodelab-firestore-rules",
    firestore: {
      rules: readFileSync("firestore.rules", "utf8"),
    },
  });
});

beforeEach(async () => {
  await testEnv.clearFirestore();
});

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

describe("tickets security rules", () => {
  it("allows the owner to create and read a ticket", async () => {
    const aliceDb = testEnv.authenticatedContext("alice").firestore();
    const ticketRef = doc(aliceDb, "tickets/ticket-1");

    await assertSucceeds(
      setDoc(ticketRef, {
        userId: "alice",
        title: "Please resend my invoice",
        body: "I cannot find the April invoice.",
        status: "open",
        createdAt: new Date(),
        updatedAt: new Date(),
      }),
    );

    await assertSucceeds(getDoc(ticketRef));
  });

  it("blocks another user from reading the ticket", async () => {
    await testEnv.withSecurityRulesDisabled(async (context) => {
      await setDoc(doc(context.firestore(), "tickets/ticket-2"), {
        userId: "alice",
        title: "Plan question",
        body: "I want to confirm my current plan.",
        status: "open",
        createdAt: new Date(),
        updatedAt: new Date(),
      });
    });

    const bobDb = testEnv.authenticatedContext("bob").firestore();
    await assertFails(getDoc(doc(bobDb, "tickets/ticket-2")));
  });

  it("blocks status changes from the web client", async () => {
    await testEnv.withSecurityRulesDisabled(async (context) => {
      await setDoc(doc(context.firestore(), "tickets/ticket-3"), {
        userId: "alice",
        title: "Cannot sign in",
        body: "Google sign-in returns an error.",
        status: "open",
        createdAt: new Date(),
        updatedAt: new Date(),
      });
    });

    const aliceDb = testEnv.authenticatedContext("alice").firestore();
    await assertFails(
      updateDoc(doc(aliceDb, "tickets/ticket-3"), {
        status: "closed",
        updatedAt: new Date(),
      }),
    );
  });

  it("keeps the test runner alive", () => {
    expect(testEnv).toBeDefined();
  });
});
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "firebase:use:dev": "firebase use dev",
    "firebase:emulators": "firebase emulators:start --only auth,firestore,functions,hosting",
    "test:rules": "firebase emulators:exec --only firestore \"vitest run tests/firestore.rules.test.ts\"",
    "deploy:stg": "firebase use stg && npm run build && firebase deploy --only hosting,firestore:rules,firestore:indexes,functions",
    "deploy:prod": "firebase use prod && npm run build && firebase deploy --only hosting,firestore:rules,firestore:indexes,functions"
  }
}

Cloud Functions 负责特权操作

Cloud Functions 适合处理不能信任浏览器的逻辑,例如状态变更、外部 API 密钥、通知和聚合。

// functions/src/index.ts
import { initializeApp } from "firebase-admin/app";
import { FieldValue, getFirestore } from "firebase-admin/firestore";
import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { HttpsError, onCall } from "firebase-functions/v2/https";

initializeApp();

const db = getFirestore();

export const closeTicket = onCall(
  {
    region: "asia-northeast1",
  },
  async (request) => {
    if (!request.auth) {
      throw new HttpsError("unauthenticated", "Sign-in is required.");
    }

    const ticketId = request.data?.ticketId;
    if (typeof ticketId !== "string" || ticketId.length > 100) {
      throw new HttpsError("invalid-argument", "ticketId is invalid.");
    }

    const ticketRef = db.doc(`tickets/${ticketId}`);
    const ticketSnap = await ticketRef.get();

    if (!ticketSnap.exists) {
      throw new HttpsError("not-found", "Ticket was not found.");
    }

    const ticket = ticketSnap.data();
    if (ticket?.userId !== request.auth.uid) {
      throw new HttpsError("permission-denied", "You cannot close this ticket.");
    }

    await ticketRef.update({
      status: "closed",
      closedAt: FieldValue.serverTimestamp(),
      updatedAt: FieldValue.serverTimestamp(),
    });

    return { ok: true };
  },
);

export const notifyTicketCreated = onDocumentCreated(
  {
    document: "tickets/{ticketId}",
    region: "asia-northeast1",
  },
  async (event) => {
    const ticket = event.data?.data();
    if (!ticket) return;

    await db.collection("adminNotifications").add({
      type: "ticket_created",
      ticketId: event.params.ticketId,
      title: ticket.title,
      userId: ticket.userId,
      createdAt: FieldValue.serverTimestamp(),
      read: false,
    });
  },
);

最重要的审查点是:Admin SDK 会绕过 Firestore Security Rules。因此函数内部必须检查 request.auth、输入类型和文档所有者。

Hosting、成本和安全检查

发布前建议先走预览,再到预发环境,最后由人工确认生产部署。

firebase login
firebase use dev
npm run build
firebase emulators:start --only auth,firestore,functions,hosting
firebase hosting:channel:deploy preview-firebase-ticket
firebase use stg
firebase deploy --only hosting,firestore:rules,firestore:indexes,functions

常见坑包括:把规则写成所有登录用户可读写;先全量读取再前端筛选;Functions 使用 Admin SDK 却不做所有者检查;Emulator 没有加载同一份 firestore.rules;以及 .env.localfirebase use、Firebase Console 指向不同项目。

成本方面,不要根据旧文章记忆价格,最新信息要看 Firebase pricing。审查 Firestore 读取次数、分页 limit、实时监听是否取消、Functions 区域和超时、日志量、Hosting 缓存。安全方面,不要把服务账号 JSON、CI token 或生产 Owner 权限交给 Claude Code;让它只改指定文件、运行本地测试并报告差异。

可直接使用的 Claude Code 提示词

实现 Firebase 支持工单功能。

范围:
- Vite + React + TypeScript
- Firebase Auth, Firestore, Cloud Functions v2, Hosting
- 只编辑 src/lib/firebase.ts, src/lib/useAuth.tsx, src/lib/tickets.ts, firestore.rules, functions/src/index.ts, firebase.json

要求:
- Google 登录用户可以创建 tickets
- 用户只能读取自己的 tickets
- Web 客户端不能修改 status
- Callable Function 在校验所有者后关闭 ticket
- 增加 Emulator Suite 的允许和拒绝测试
- dev/stg/prod 项目不能混用

禁止:
- 创建、打印或保存服务账号 JSON
- 执行生产部署
- 使用 allow read, write: if true

报告:
- 修改文件
- 执行的测试
- 规则中的权限边界
- 剩余人工确认项

总结

Claude Code 很适合 Firebase 开发,因为规则、函数、索引、Hosting 配置和客户端 SDK 调用都能形成清晰差异。但权限、生产部署、数据归属和成本边界仍然需要人工判断。

实际项目中,建议先在 Emulator Suite 中完成一个纵向切片,加入 Security Rules 的失败测试,审查 Cloud Functions 的授权逻辑,再通过 Hosting 预览频道发布。ClaudeCodeLab 可以协助 Firebase 实装、Security Rules 审查、Emulator Suite 培训和团队开发规范建设。如果需要咨询或培训,请准备当前 Firebase 服务、要上线的功能和现有部署流程。

实际尝试本文内容时,请确认你正在使用开发项目,firebase use.env.local 一致,Security Rules 的成功和失败测试都通过,Cloud Functions 内部做了所有者验证,并且 Claude Code 没有被允许自动执行生产部署。

#Claude Code #Firebase #Firestore #Cloud Functions #BaaS
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。