Desenvolvimento Firebase com Claude Code: guia prático de implementação
Use Claude Code com Firebase: Auth, regras Firestore, Functions, Hosting, Emulator Suite, ambientes, custos e segurança com código real.
O que definir antes de pedir Firebase ao Claude Code
Firebase é uma plataforma BaaS útil para equipes que querem autenticação, banco de dados, funções serverless e hospedagem sem manter um backend completo. A combinação prática costuma ser Firebase Authentication, Cloud Firestore, Cloud Functions for Firebase, Firebase Hosting e Local Emulator Suite.
O risco é que um app Firebase pode estar funcionando e ainda não estar seguro. Firestore Security Rules abertas demais podem expor documentos de outros usuários. O Admin SDK usado em Cloud Functions ignora as Security Rules, então a autorização dentro da função precisa ser revisada separadamente. E confundir projeto de desenvolvimento com produção pode transformar um teste local em dados reais e custo real.
Com Claude Code, peça uma fatia vertical de funcionalidade. Um recurso de tickets de suporte, por exemplo, deve incluir UI, sessão do usuário, escrita no Firestore, Security Rules, testes no Emulator Suite, Cloud Function para alterações privilegiadas e configuração do Hosting.
Para bases relacionadas, veja o guia de autenticação, o guia de CI/CD e a integração com Supabase se quiser comparar 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
Os exemplos usam Vite + React + TypeScript. Em Next.js ou Astro, a lógica é a mesma, mas os nomes de variáveis e rotas mudam.
Arquitetura mínima e documentação oficial
Use a documentação oficial como fonte principal. Em Firebase, regras e comportamento de emuladores são sensíveis; copiar snippets antigos pode abrir permissões demais.
| Área | Link oficial | Bom escopo para Claude Code |
|---|---|---|
| Authentication | Firebase Authentication | UI de login, perfil, estado de sessão |
| Firestore | Cloud Firestore | Coleções, consultas, índices |
| Security Rules | Firestore Security Rules | Regras, testes negativos, checagem de dono |
| Cloud Functions | Cloud Functions for Firebase | Validação servidor, notificações, agregações |
| Hosting | Firebase Hosting | Deploy SPA, cache, preview |
| Emulator | Local Emulator Suite | Testes locais, cobertura de regras, CI |
| Preços | Firebase pricing | Leituras, escritas, invocações, logs |
| Claude Code | Claude Code docs | Divisão de tarefas, revisão e testes |
Uma estrutura fácil de revisar:
.
├─ firebase.json
├─ firestore.rules
├─ firestore.indexes.json
├─ .firebaserc
├─ functions/
│ ├─ package.json
│ └─ src/index.ts
└─ src/
├─ lib/firebase.ts
├─ lib/tickets.ts
└─ lib/useAuth.tsx
Separe ambientes primeiro
Muitos problemas de Firebase são confusão de projeto. Defina dev, staging e produção antes de escrever código de negócio.
{
"projects": {
"dev": "claudecodelab-firebase-dev",
"stg": "claudecodelab-firebase-stg",
"prod": "claudecodelab-firebase-prod"
}
}
firebase.json guarda regras, índices, Functions, Hosting e emuladores.
{
"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
}
}
Em Vite, .env.local contém a configuração pública do SDK Web. A Web API key não é segredo de servidor, mas service account JSON é segredo.
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 e inicialização do cliente
Este arquivo inicializa App, Auth, Firestore e Functions, e conecta emuladores no desenvolvimento.
// 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;
}
O estado de login fica melhor em um Hook. No primeiro login, gravamos users/{uid} com merge.
// 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),
};
}
Prompt recomendado:
Implemente login Google com Firebase Auth.
- Stack Vite + React + TypeScript
- Reuse src/lib/firebase.ts
- No primeiro login, grave users/{uid} com merge
- Retorne função logout
- Não crie nem mostre credenciais de service account
- Explique erros de tipo, impacto em Firestore Rules e checagens manuais
Modelo Firestore e CRUD
O exemplo é um sistema de tickets de suporte. Ele cobre três casos reais: portal de membros, fluxo de atendimento com fechamento via Function e painel interno com notificações ao criar 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">),
}));
}
Essa consulta pode exigir índice composto.
{
"indexes": [
{
"collectionGroup": "tickets",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "DESCENDING"
}
]
}
],
"fieldOverrides": []
}
Firestore Security Rules não são filtro
Security Rules não filtram uma consulta ampla para devolver só documentos seguros. A consulta precisa ser escrita para retornar apenas documentos permitidos.
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;
}
}
}
Revise a comparação entre UID e dono, lista fechada de campos, bloqueio explícito de exclusão e ausência de acesso cliente a dados administrativos.
Testes com 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 para operações privilegiadas
Mudanças de estado, chaves externas, notificações e agregações não devem depender do navegador.
// 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,
});
},
);
Ponto crítico: o Admin SDK ignora Security Rules. Toda Callable Function precisa validar request.auth, entrada e dono do documento.
Hosting, custos e segurança
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
Falhas comuns: permitir tudo para qualquer usuário logado, ler a coleção inteira e filtrar no React, esquecer checagem de dono em Functions, testar com emulador sem carregar as mesmas regras, ou misturar .env.local, firebase use e Console.
Custos devem ser revisados em Firebase pricing. Observe leituras do Firestore, limit, listeners em tempo real, região e timeout de Functions, volume de logs e cache do Hosting. Em segurança, não coloque service account JSON, tokens de CI ou papel Owner de produção em prompts do Claude Code.
Modelo de prompt para Claude Code
Implemente o recurso Firebase de tickets de suporte.
Escopo:
- Vite + React + TypeScript
- Firebase Auth, Firestore, Cloud Functions v2, Hosting
- Editar apenas src/lib/firebase.ts, src/lib/useAuth.tsx, src/lib/tickets.ts, firestore.rules, functions/src/index.ts, firebase.json
Requisitos:
- Usuários com Google login podem criar tickets
- Usuários só leem seus próprios tickets
- Cliente Web não altera status
- Callable Function fecha ticket após validar dono
- Adicionar testes Emulator Suite para acessos permitidos e negados
- Manter dev/stg/prod separados
Proibido:
- Criar, imprimir ou salvar service account JSON
- Executar deploy de produção
- Usar allow read, write: if true
Relatório:
- Arquivos alterados
- Testes executados
- Limite de permissão nas rules
- Checagens manuais restantes
Conclusão
Claude Code combina bem com Firebase porque regras, funções, índices, Hosting e chamadas do SDK viram diffs fáceis de revisar. O julgamento humano continua essencial em permissões, deploy de produção, propriedade de dados e limites de custo.
Em projetos reais, crie uma fatia pequena no Emulator Suite, adicione testes negativos de Security Rules, revise autorização em Cloud Functions e publique em preview antes de staging e produção. A ClaudeCodeLab pode apoiar implementação Firebase, revisão de Security Rules, treinamento em Emulator Suite e fluxos de equipe com Claude Code.
Ao testar este guia, confirme que usa o projeto Firebase de desenvolvimento, que firebase use corresponde a .env.local, que os testes positivos e negativos de regras passam, que Cloud Functions valida o dono internamente e que Claude Code não pode executar deploy de produção automaticamente.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.