Use Cases (Updated: 6/1/2026)

Claude Code and Firebase Development: A Practical Implementation Guide

Build Firebase apps with Claude Code: Auth, Firestore rules, Functions, Hosting, Emulator Suite, environments, costs, and security.

Claude Code and Firebase Development: A Practical Implementation Guide

What to decide before asking Claude Code to build on Firebase

Firebase is a strong BaaS for small teams that want authentication, a document database, serverless functions, and hosting without operating a full backend. The productive stack is usually Firebase Authentication, Cloud Firestore, Cloud Functions for Firebase, Firebase Hosting, and the Local Emulator Suite.

The risk is that a Firebase app can look complete while still being unsafe. Weak Firestore Security Rules can expose another user’s documents. The Admin SDK used inside Cloud Functions bypasses Security Rules, so server-side checks must be reviewed separately. A mixed-up development and production project can turn a local test into real writes and real cost.

When Claude Code helps with Firebase development, give it a vertical slice rather than a vague screen request. For example, “build a support ticket form” should include the UI, authenticated write path, Firestore Security Rules, rule tests, a Cloud Function for privileged changes, and Hosting configuration.

For related foundations, see the Claude Code authentication implementation guide, the CI/CD setup guide, and the Supabase integration guide if you want to compare backend choices.

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

This guide uses Vite, React, and TypeScript because the code is easy to copy and verify. The same ideas apply to Next.js or Astro, but you will adapt environment variable names and routing.

Minimum architecture and primary sources

Use the official Firebase documentation as the source of truth. Security Rules and emulator behavior can change in details, and old snippets can be dangerous when copied blindly.

AreaOfficial linkGood Claude Code task boundary
AuthenticationFirebase AuthenticationLogin UI, user profile creation, auth state handling
FirestoreCloud FirestoreCollection design, query functions, index assumptions
Security RulesFirestore Security RulesRules, negative tests, owner checks
Cloud FunctionsCloud Functions for FirebaseServer-side validation, notifications, aggregation
HostingFirebase HostingSPA deployment, caching, preview channels
Emulator SuiteLocal Emulator SuiteLocal verification, rule coverage, CI tests
PricingFirebase pricingReads, writes, function invocations, log volume
Claude CodeClaude Code docsTask sizing, review loop, test execution

This is the project shape I normally ask Claude Code to inspect before making changes:

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

The instruction should be: reuse the existing pattern when it exists, add the missing files when it does not, and do not deploy production.

Lock environment separation first

Many Firebase incidents are not complex bugs. They are project mix-ups. A developer thinks they are using dev, but the web app or CLI points at production. Define dev, staging, and production before asking Claude Code to generate feature code.

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

Keep Firestore rules, indexes, Functions, Hosting, and emulator ports in firebase.json.

{
  "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
  }
}

For a Vite app, keep public web configuration in .env.local. The Firebase web API key is not a server secret, but service account JSON and Admin SDK credentials are secrets and must never be placed in frontend code.

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 and client initialization

The following src/lib/firebase.ts initializes Firebase App, Authentication, Firestore, and Functions, then connects to local emulators during development.

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

Then wrap authentication state in a hook. Creating users/{uid} on first sign-in makes later ownership rules easier to reason about.

// 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),
  };
}

A useful Claude Code prompt is specific about files, security, and verification:

Implement Google sign-in with Firebase Auth.
- Target stack: Vite + React + TypeScript
- Reuse the existing src/lib/firebase.ts initialization
- Merge a users/{uid} profile on first login
- Return a logout function
- Do not create, display, or store service account credentials
- After implementation, explain type errors, Firestore rule impact, and manual checks

Firestore data model and CRUD

The example feature is a support ticket system. It covers three practical use cases: a member portal where users see their own tickets, a support workflow where a privileged function closes tickets, and an internal dashboard where new tickets create notifications and aggregates.

Client code should only create and list the current user’s tickets. Firestore charges and latency are both tied to document access, so avoid broad queries that the UI filters afterward.

// 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">),
  }));
}

The where plus orderBy query can require a composite index. Keep the index in source control instead of relying only on a console click.

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

Firestore Security Rules are not filters

Security Rules do not filter a broad query down to safe documents. A query must be shaped so every possible result is allowed. That is why getDocs(collection(db, "tickets")) followed by client-side filtering is both unsafe and expensive.

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;
    }
  }
}

Review the comparison between request.auth.uid and resource.data.userId, the strict field list, explicit delete denial, and the fact that admin-only state is not opened to the web client.

Test rules with the Emulator Suite

Rule tests turn security assumptions into repeatable checks. Install the dependencies:

npm install -D vitest @firebase/rules-unit-testing firebase
firebase setup:emulators:firestore

Create tests/firestore.rules.test.ts.

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

Add scripts so local and CI use the same path.

{
  "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"
  }
}

Move privileged work to Cloud Functions

Use Cloud Functions for tasks that must not be trusted to the browser: privileged state changes, external API keys, notifications, and aggregation.

// 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,
    });
  },
);

The key review point is that the Admin SDK bypasses Firestore Security Rules. Claude Code must add input validation and owner checks inside the function, not assume the client rules still protect the write.

Hosting and pre-deploy checks

Hosting is fast to ship, so the review path needs to be explicit: preview first, staging second, production only after human confirmation.

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

Review the active firebase use target, project IDs in .firebaserc, the Hosting public directory, SPA rewrites, asset cache headers, Functions region, and whether rules and indexes deploy with the app.

Specific failure cases

The first common failure is allow read, write: if request.auth != null;. That means every signed-in user can reach every document. Owner checks must compare the authenticated UID with stored ownership.

The second is client-side filtering. Fetching every ticket and filtering in React is insecure and wasteful. Shape the query to the allowed data.

The third is forgetting that Cloud Functions Admin SDK writes bypass Security Rules. Callable Functions need request.auth, input validation, and document ownership checks.

The fourth is an emulator setup that does not load the same firestore.rules used in production. Keep firebase.json, test setup, and deploy scripts aligned.

The fifth is project confusion. Check firebase use, .env.local, and the Firebase Console URL before running writes or deploy commands.

Cost and security review

Firebase pricing can change, so verify current numbers on the official pricing page instead of designing from old blog posts. Review Firestore reads, page limits, realtime listener cleanup, Cloud Functions region and timeout, log volume, and Hosting cache behavior.

Security review should cover service account handling, CI secrets, production IAM roles, App Check where appropriate, and the boundary between client SDKs and Admin SDKs. A practical Claude Code permission model is: edit only the requested files, do not deploy, run local tests, and report the diff plus remaining manual checks.

A prompt template that works

Implement the Firebase support ticket feature.

Scope:
- Vite + React + TypeScript
- Firebase Auth, Firestore, Cloud Functions v2, Hosting
- Only edit src/lib/firebase.ts, src/lib/useAuth.tsx, src/lib/tickets.ts, firestore.rules, functions/src/index.ts, firebase.json

Requirements:
- Signed-in Google users can create tickets
- Users can read only their own tickets
- Web clients cannot change ticket status
- A callable function closes tickets after owner validation
- Add Emulator Suite tests for allowed and denied Firestore access
- Keep dev, staging, and production Firebase projects separated

Do not:
- Create, print, or store service account JSON
- Run production deploys
- Use allow read, write: if true

Report:
- Changed files
- Tests run
- Permission boundary in the rules
- Remaining manual checks

Conclusion

Claude Code is useful for Firebase development because Firebase work is file-driven: rules, functions, indexes, hosting config, tests, and client SDK calls all produce reviewable diffs. The parts that still need human judgment are permissions, production deploys, data ownership, and cost boundaries.

For real projects, build one vertical slice in the Emulator Suite, add negative Security Rules tests, review Cloud Functions authorization, deploy to a preview channel, then promote to staging and production.

ClaudeCodeLab can help with Firebase implementation, Security Rules review, Emulator Suite training, and team workflows for Claude Code. For a consulting or training request, share your current Firebase services, the feature you want to ship, and how deploys are handled today.

When you try the steps from this article, confirm that you are using a development Firebase project, firebase use matches .env.local, Security Rules tests pass for both success and failure cases, Cloud Functions validate ownership internally, and Claude Code is not allowed to run production deployment commands automatically.

#Claude Code #Firebase #Firestore #Cloud Functions #BaaS
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.