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é.
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.
| Domaine | Lien officiel | Bon périmètre pour Claude Code |
|---|---|---|
| Authentication | Firebase Authentication | UI de connexion, profil utilisateur, état de session |
| Firestore | Cloud Firestore | Collections, requêtes, index |
| Security Rules | Firestore Security Rules | Règles, tests négatifs, contrôle propriétaire |
| Cloud Functions | Cloud Functions for Firebase | Validation serveur, notifications, agrégations |
| Hosting | Firebase Hosting | Déploiement SPA, cache, preview |
| Emulator | Local Emulator Suite | Tests locaux, couverture règles, CI |
| Prix | Firebase pricing | Lectures, écritures, invocations, logs |
| Claude Code | Claude Code docs | Dé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.
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.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.