Use Cases (Mis à jour: 01/06/2026)

Développer Firebase avec Claude Code : guide pratique complet

Utilisez Claude Code avec Firebase : Auth, règles Firestore, Functions, Hosting, Emulator Suite, environnements, coûts et sécurité.

Développer Firebase avec Claude Code : guide pratique complet

Ce qu’il faut décider avant de confier Firebase à Claude Code

Firebase est une plateforme BaaS efficace pour livrer vite une application avec authentification, base de données, fonctions serveur et hébergement. La combinaison la plus courante réunit Firebase Authentication, Cloud Firestore, Cloud Functions for Firebase, Firebase Hosting et Local Emulator Suite.

Le piège est qu’une application Firebase peut fonctionner tout en restant dangereuse. Des Firestore Security Rules trop larges peuvent exposer les documents d’autres utilisateurs. L’Admin SDK utilisé dans Cloud Functions contourne les Security Rules, donc les validations serveur doivent être relues séparément. Et une confusion entre projet de développement et projet de production peut transformer un test local en écritures réelles.

Avec Claude Code, il faut demander une tranche verticale de fonctionnalité plutôt qu’un simple écran. Par exemple, une fonction de tickets de support doit inclure l’interface, la session utilisateur, l’écriture Firestore, les Security Rules, les tests d’émulateur, une Cloud Function pour les opérations privilégiées et la configuration Hosting.

Pour les bases connexes, consultez le guide d’authentification avec Claude Code, le guide CI/CD et le guide d’intégration Supabase si vous comparez les backends.

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

Les exemples utilisent Vite + React + TypeScript. Avec Next.js ou Astro, la logique reste la même, mais les variables d’environnement et le routing changent.

Architecture minimale et sources officielles

Les documentations officielles doivent rester la référence, surtout pour les règles et les émulateurs. Un ancien extrait de blog peut encore compiler mais ouvrir trop de droits.

DomaineLien officielBon périmètre pour Claude Code
AuthenticationFirebase AuthenticationUI de connexion, profil utilisateur, état de session
FirestoreCloud FirestoreCollections, requêtes, index
Security RulesFirestore Security RulesRègles, tests négatifs, contrôle propriétaire
Cloud FunctionsCloud Functions for FirebaseValidation serveur, notifications, agrégations
HostingFirebase HostingDéploiement SPA, cache, preview
EmulatorLocal Emulator SuiteTests locaux, couverture règles, CI
PrixFirebase pricingLectures, écritures, invocations, logs
Claude CodeClaude Code docsDécoupage, revue, exécution de tests

Une structure lisible pour Claude Code ressemble à ceci :

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

Séparer les environnements dès le départ

Beaucoup d’incidents Firebase sont des erreurs d’environnement. Définissez dev, staging et production avant d’écrire la fonctionnalité, puis interdisez clairement à Claude Code de déployer en production.

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

firebase.json centralise les règles, les index, Functions, Hosting et les émulateurs.

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

Dans Vite, .env.local contient la configuration publique du SDK Web. La clé Web Firebase n’est pas un secret serveur, mais un service account JSON l’est.

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 et initialisation client

Ce fichier initialise App, Auth, Firestore et Functions, puis connecte les émulateurs en développement.

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

Le Hook suivant gère la session et crée users/{uid} lors de la première connexion.

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

Demande recommandée à Claude Code :

Implémente Google sign-in avec Firebase Auth.
- Stack Vite + React + TypeScript
- Réutilise src/lib/firebase.ts
- Crée users/{uid} avec merge au premier login
- Retourne une fonction logout
- Ne crée ni n'affiche de credentials service account
- Explique les erreurs de type, l'impact sur Firestore Rules et les vérifications manuelles

Modèle Firestore et CRUD

L’exemple est un système de tickets de support. Il couvre trois cas concrets : portail membre, workflow support avec fermeture par Function, et tableau interne qui reçoit des notifications à la création.

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

La requête peut demander un index composé. Conservez-le dans le dépôt.

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

Les Security Rules ne filtrent pas

Les règles ne transforment pas une requête large en résultat sûr. La requête elle-même doit ne pouvoir retourner que des documents autorisés.

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

Vérifiez la comparaison entre UID et propriétaire, la liste fermée de champs, l’interdiction de suppression et l’absence d’accès client aux données administratives.

Tester avec 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"
  }
}

Déplacer les opérations privilégiées dans Cloud Functions

Les changements d’état, clés externes, notifications et agrégations ne doivent pas dépendre du navigateur.

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

Le point critique : l’Admin SDK contourne les Firestore Security Rules. Il faut donc valider request.auth, les entrées et le propriétaire dans la fonction.

Hosting, coûts et sécurité

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

Les échecs fréquents sont concrets : autoriser tous les utilisateurs connectés, lire toute la collection puis filtrer côté React, oublier le contrôle propriétaire dans Functions, tester avec un émulateur qui ne charge pas les mêmes règles ou mélanger .env.local, firebase use et la console.

Pour les coûts, vérifiez la page Firebase pricing. Relisez les lectures Firestore, limit, listeners temps réel, région et timeout des Functions, volume de logs et cache Hosting. Côté sécurité, ne mettez jamais service account JSON, token CI ou rôle Owner de production dans une consigne Claude Code.

Modèle de prompt Claude Code

Implémente la fonctionnalité de tickets Firebase.

Périmètre:
- Vite + React + TypeScript
- Firebase Auth, Firestore, Cloud Functions v2, Hosting
- Modifier uniquement src/lib/firebase.ts, src/lib/useAuth.tsx, src/lib/tickets.ts, firestore.rules, functions/src/index.ts, firebase.json

Exigences:
- Les utilisateurs connectés avec Google peuvent créer des tickets
- Chaque utilisateur lit seulement ses tickets
- Le client Web ne peut pas changer status
- Une Callable Function ferme le ticket après contrôle propriétaire
- Ajouter des tests Emulator Suite pour accès autorisés et refusés
- Garder dev, stg et prod séparés

Interdit:
- Créer, afficher ou stocker un service account JSON
- Lancer un déploiement production
- Utiliser allow read, write: if true

Rapport:
- Fichiers modifiés
- Tests exécutés
- Frontière de permissions
- Vérifications manuelles restantes

Conclusion

Claude Code est efficace avec Firebase parce que règles, fonctions, index, Hosting et appels SDK produisent des diffs relisibles. Les décisions à garder côté humain sont les permissions, le déploiement production, la propriété des données et la limite de coûts.

En projet réel, construisez une petite tranche dans Emulator Suite, ajoutez des tests négatifs de Security Rules, relisez l’autorisation Cloud Functions, publiez en preview puis seulement ensuite en staging et production. ClaudeCodeLab peut accompagner l’implémentation Firebase, la revue Security Rules, la formation Emulator Suite et les workflows d’équipe avec Claude Code.

Quand vous testez ce guide, confirmez que vous utilisez le projet Firebase de développement, que firebase use correspond à .env.local, que les tests de règles passent en succès et en échec, que Cloud Functions valide le propriétaire en interne et que Claude Code ne peut pas lancer automatiquement le déploiement production.

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

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.