Use Cases (Aktualisiert: 1.6.2026)

Firebase-Entwicklung mit Claude Code: Praxisguide für Auth, Rules und Deploys

Firebase mit Claude Code umsetzen: Auth, Firestore Rules, Functions, Hosting, Emulator Suite, Umgebungen, Kosten und Sicherheit.

Firebase-Entwicklung mit Claude Code: Praxisguide für Auth, Rules und Deploys

Was vor der Firebase-Implementierung mit Claude Code feststehen muss

Firebase ist ein praktischer BaaS-Stack für Teams, die Authentifizierung, Datenbank, serverseitige Funktionen und Hosting schnell kombinieren wollen. Typisch sind Firebase Authentication, Cloud Firestore, Cloud Functions for Firebase, Firebase Hosting und die Local Emulator Suite.

Die Gefahr liegt darin, dass eine Firebase-App schnell funktioniert, aber noch nicht betriebssicher ist. Zu offene Firestore Security Rules können fremde Nutzerdaten freigeben. Das Admin SDK in Cloud Functions umgeht Security Rules, daher braucht jede Funktion eigene Prüfungen. Und wenn Entwicklungs- und Produktionsprojekt verwechselt werden, landen lokale Tests in echten Daten und echten Kosten.

Claude Code sollte deshalb nicht nur einen Bildschirm bauen, sondern eine vertikale Funktion. Beim Beispiel “Support-Ticket” gehören UI, Loginstatus, Firestore-Schreibpfad, Security Rules, Regeltests, eine Cloud Function für privilegierte Änderungen und Hosting-Konfiguration zusammen.

Als Grundlage passen der Guide zur Authentifizierung, der CI/CD-Guide und zum Vergleich anderer Backends die Supabase-Integration.

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

Die Beispiele verwenden Vite + React + TypeScript. In Next.js oder Astro bleibt das Muster gleich, nur Variablen und Routing ändern sich.

Architektur und offizielle Quellen

Nutzen Sie die offizielle Dokumentation als Primärquelle. Besonders Security Rules, Emulatoren und Deploy-Verhalten sollten nicht aus alten Snippets übernommen werden.

BereichOffizieller LinkSinnvoller Auftrag an Claude Code
AuthenticationFirebase AuthenticationLogin-UI, Nutzerprofil, Session State
FirestoreCloud FirestoreCollections, Queries, Indexannahmen
Security RulesFirestore Security RulesRegeln, Negativtests, Owner-Prüfung
Cloud FunctionsCloud Functions for FirebaseServervalidierung, Benachrichtigungen, Aggregation
HostingFirebase HostingSPA-Auslieferung, Cache, Preview Channels
EmulatorLocal Emulator SuiteLokale Tests, Rule Coverage, CI
PreiseFirebase pricingReads, Writes, Function-Aufrufe, Logs
Claude CodeClaude Code docsTaskgröße, Review, Testausführung

Eine gute Projektstruktur ist:

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

Umgebungstrennung zuerst festlegen

Viele Firebase-Probleme sind keine komplizierten Bugs, sondern Projektverwechslungen. Dev, Staging und Produktion sollten vor dem Feature-Code getrennt sein.

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

firebase.json versioniert Firestore-Regeln, Indexe, Functions, Hosting und Emulatoren.

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

Für Vite sieht .env.local so aus. Die Web API key ist kein Servergeheimnis, ein Service-Account-JSON aber schon.

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 und Client-Initialisierung

Diese Datei initialisiert App, Auth, Firestore und Functions und verbindet in der Entwicklung die Emulatoren.

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

Der Auth-State wird als Hook gekapselt. Beim ersten Login wird users/{uid} per merge geschrieben.

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

Gute Anweisung an Claude Code:

Implementiere Google sign-in mit Firebase Auth.
- Stack: Vite + React + TypeScript
- Nutze die bestehende src/lib/firebase.ts Initialisierung
- Schreibe beim ersten Login users/{uid} mit merge
- Gib eine logout-Funktion zurück
- Erzeuge oder zeige keine Service-Account-Credentials
- Berichte Type-Fehler, Auswirkung auf Firestore Rules und manuelle Checks

Firestore-Datenmodell und CRUD

Das Beispiel ist ein Support-Ticket-System. Es deckt drei reale Fälle ab: Mitglieder sehen eigene Tickets, Support schließt Tickets über eine Function und ein internes Dashboard erhält Benachrichtigungen bei neuen Tickets.

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

Die Query kann einen zusammengesetzten Index brauchen.

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

Firestore Security Rules sind keine Filter

Rules filtern keine zu breite Query auf sichere Dokumente herunter. Die Query muss selbst so geformt sein, dass alle möglichen Ergebnisse erlaubt sind.

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

Prüfen Sie Owner-Vergleich, geschlossene Feldliste, explizites Löschverbot und dass Admin-Daten nicht an den Client geöffnet werden.

Tests mit der 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"
  }
}

Privilegierte Logik in Cloud Functions

Statuswechsel, externe API-Schlüssel, Benachrichtigungen und Aggregationen gehören nicht in den Browser.

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

Wichtig: Das Admin SDK umgeht Security Rules. Jede Callable Function braucht request.auth, Eingabevalidierung und Owner-Prüfung.

Hosting, Kosten und Sicherheit

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

Typische Fehler sind: alle eingeloggten Nutzer dürfen alles lesen, die UI filtert nach einem Vollscan, Functions prüfen den Owner nicht, der Emulator lädt andere Regeln oder .env.local und firebase use zeigen auf verschiedene Projekte.

Kosten sollten über Firebase pricing aktuell geprüft werden. Reviewpunkte sind Firestore Reads, limit, Realtime Listener, Functions-Region und Timeout, Logvolumen und Hosting-Cache. Sicherheitsseitig gehören Service-Account-JSON, CI-Token und Produktions-Owner-Rollen nicht in einen Claude-Code-Prompt.

Prompt-Vorlage für Claude Code

Implementiere die Firebase Support-Ticket-Funktion.

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

Anforderungen:
- Google-Login-Nutzer können tickets erstellen
- Nutzer können nur eigene tickets lesen
- Web-Clients können status nicht ändern
- Eine Callable Function schließt tickets nach Owner-Prüfung
- Emulator-Suite-Tests für erlaubte und abgelehnte Zugriffe ergänzen
- dev/stg/prod getrennt halten

Verboten:
- Service-Account-JSON erzeugen, ausgeben oder speichern
- Produktions-Deploy ausführen
- allow read, write: if true verwenden

Bericht:
- Geänderte Dateien
- Ausgeführte Tests
- Berechtigungsgrenze der Rules
- Offene manuelle Checks

Fazit

Claude Code passt gut zu Firebase, weil Regeln, Functions, Indexe, Hosting und Client-SDK-Aufrufe als prüfbare Diffs entstehen. Menschliche Kontrolle bleibt bei Berechtigungen, Produktionsdeploys, Datenbesitz und Kostenrahmen nötig.

In echten Projekten baue ich zuerst eine kleine Funktion in der Emulator Suite, ergänze negative Security-Rules-Tests, prüfe die Cloud-Functions-Autorisierung und veröffentliche erst über Preview, dann Staging und Produktion. ClaudeCodeLab unterstützt Firebase-Implementierung, Security-Rules-Reviews, Emulator-Suite-Training und Team-Workflows mit Claude Code.

Wenn Sie diesen Guide ausprobieren, prüfen Sie: Entwicklungsprojekt aktiv, firebase use passend zu .env.local, positive und negative Rule-Tests grün, Owner-Prüfung in Cloud Functions vorhanden und keine automatische Produktionsbereitstellung durch Claude Code.

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

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.