Schema Firestore com Claude Code: guia GCP/Firebase para SaaS
Modele collections, rules, índices e dados SaaS no Firestore com Claude Code evitando falhas comuns.
O design do Firestore começa pelas leituras
Sou Masa, operador do claudecode-lab.com.
Meu primeiro erro com Firestore foi começar por nomes de collections. users, projects, events, subscriptions pareciam bonitos. Depois vieram as telas reais de um SaaS: dashboard de projetos, membros com permissões, eventos recentes, trial terminando, cobrança, plano ativo e painel administrativo. O schema parecia limpo, mas as queries ficaram estranhas.
Firestore não é um banco relacional onde você corrige tudo depois com JOIN. A documentação oficial do modelo de dados explica que os dados ficam em documents dentro de collections. Um document pode conter campos, objetos e subcollections. Uma analogia simples: collection é uma prateleira, document é uma pasta, subcollection é uma pasta menor dentro dela.
Por isso a ordem saudável é: telas, queries, schema, Security Rules e índices. Claude Code é muito útil como revisor local. Eu não peço apenas “crie um schema”; peço para comparar firestore.rules, firestore.indexes.json, tipos TypeScript e funções de consulta, procurando contradições.
Collections, documents e subcollections em um SaaS
Um SaaS B2B pequeno pode começar assim:
users/{uid}
projects/{projectId}
projects/{projectId}/members/{uid}
projects/{projectId}/events/{eventId}
subscriptions/{uid}
billingCustomers/{uid}
| Caminho | Papel | Leitura típica |
|---|---|---|
users/{uid} | Perfil do usuário | Perfil atual |
projects/{projectId} | Workspace ou projeto | Detalhe do projeto |
projects/{projectId}/members/{uid} | Role por projeto | Membros e permissão |
projects/{projectId}/events/{eventId} | Atividade e auditoria | Eventos recentes |
subscriptions/{uid} | Plano e status de cobrança | Controle de recursos |
billingCustomers/{uid} | IDs de Stripe ou billing | Jobs de servidor |
Subcollection deve existir porque facilita uma leitura frequente. Se a tela principal mostra os últimos 50 eventos de um projeto, projects/{projectId}/events é natural. Se você precisar de uma visão entre projetos, use collection group query, sabendo que isso muda rules e índices.
Prompt melhor para Claude Code:
claude -p "
Revise o design Firestore de um SaaS B2B.
Antes de sugerir collections, liste as queries por tela.
Telas:
- Projetos do usuário atual
- Detalhe do projeto
- Últimos 50 eventos do projeto
- Lista admin de status de assinatura
- Usuários cujo trial termina em breve
Para cada tela, retorne where/orderBy/limit,
Composite indexes necessários e condição de Security Rules.
"
Isso força a conversa a partir do produto, não de um diagrama bonito.
Exemplo de schema SaaS com usuários, projetos, eventos e cobrança
Este modelo funciona bem com Firebase Admin SDK ou Cloud Functions. Mesmo que parte da escrita venha do cliente, os tipos ajudam a definir o contrato.
import type { Timestamp } from "firebase-admin/firestore";
export type ProjectRole = "owner" | "admin" | "member" | "viewer";
export type SubscriptionStatus =
| "trialing"
| "active"
| "past_due"
| "canceled";
export interface ProjectDoc {
id: string;
name: string;
ownerUid: string;
plan: "free" | "starter" | "pro";
memberCount: number;
lastEventAt: Timestamp | null;
createdAt: Timestamp;
updatedAt: Timestamp;
}
export interface ProjectMemberDoc {
uid: string;
role: ProjectRole;
displayName: string;
email: string;
joinedAt: Timestamp;
}
export interface ProjectEventDoc {
id: string;
projectId: string;
actorUid: string;
actorName: string;
type: "created" | "updated" | "commented" | "exported";
message: string;
createdAt: Timestamp;
}
export interface SubscriptionDoc {
uid: string;
status: SubscriptionStatus;
plan: "free" | "starter" | "pro";
trialEndsAt: Timestamp | null;
updatedAt: Timestamp;
}
displayName e email dentro de ProjectMemberDoc são desnormalização intencional. Desnormalizar é copiar pequenos dados de exibição para evitar leituras extras. Em Firestore isso costuma ser bom: uma lista com 50 membros não precisa ler 50 documents users/{uid} apenas para mostrar nomes. Você ganha previsibilidade e paga o custo de sincronizar quando o perfil muda.
Exemplo 1: para o dashboard inicial, uso uma coleção de referências por usuário.
users/{uid}/projectRefs/{projectId}
projectId: string
projectName: string
role: "owner" | "admin" | "member" | "viewer"
lastEventAt: Timestamp | null
Não é o modelo mais puro, mas transforma a primeira tela em uma query simples.
Security Rules não são filtros
Esse é o ponto que mais quebra projetos. A documentação oficial de queries seguras diz que rules não filtram resultados. A query é aceita ou rejeitada inteira. Se ela pode retornar um document proibido, ela falha.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /projects/{projectId}/events/{eventId} {
allow list: if request.auth != null
&& exists(/databases/$(database)/documents/projects/$(projectId)/members/$(request.auth.uid))
&& request.query.limit <= 50;
}
}
}
Esta query não atende à regra, porque não tem limit.
import { collection, getDocs } from "firebase/firestore";
await getDocs(collection(db, "projects", projectId, "events"));
A versão correta explicita a mesma restrição:
import {
collection,
getDocs,
limit,
orderBy,
query,
} from "firebase/firestore";
export async function listProjectEvents(projectId: string) {
const eventsQuery = query(
collection(db, "projects", projectId, "events"),
orderBy("createdAt", "desc"),
limit(50),
);
const snap = await getDocs(eventsQuery);
return snap.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
}
Exemplo 2: se a regra só permite visibility == "public", a query também deve ter where("visibility", "==", "public"). Firestore não devolve “apenas o que pode”; ele precisa provar que toda a query é segura.
Composite index e collection group query
Firestore cria índices simples automaticamente, mas combinações de filtros e ordenação exigem composite index. A página oficial de indexação explica que uma query sem índice pode retornar um link para criar o índice. Em projeto sério, prefiro versionar firestore.indexes.json.
{
"indexes": [
{
"collectionGroup": "events",
"queryScope": "COLLECTION_GROUP",
"fields": [
{ "fieldPath": "projectId", "order": "ASCENDING" },
{ "fieldPath": "createdAt", "order": "DESCENDING" }
]
},
{
"collectionGroup": "subscriptions",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "status", "order": "ASCENDING" },
{ "fieldPath": "trialEndsAt", "order": "ASCENDING" }
]
}
],
"fieldOverrides": []
}
Collection group query lê todas as subcollections com o mesmo ID. Para eventos de projeto:
import {
collectionGroup,
getDocs,
limit,
orderBy,
query,
where,
} from "firebase/firestore";
export async function listRecentEventsAcrossProjects(projectId: string) {
const eventsQuery = query(
collectionGroup(db, "events"),
where("projectId", "==", projectId),
orderBy("createdAt", "desc"),
limit(50),
);
const snap = await getDocs(eventsQuery);
return snap.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
}
As rules precisam acompanhar. O guia oficial de estrutura de rules lembra que match aponta para document paths, não para collections. Para collection group query, use rules version 2 e wildcard recursivo.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() {
return request.auth != null;
}
function isProjectMember(projectId) {
return signedIn()
&& exists(/databases/$(database)/documents/projects/$(projectId)/members/$(request.auth.uid));
}
match /{path=**}/events/{eventId} {
allow list: if signedIn()
&& request.query.limit <= 50
&& resource.data.projectId is string
&& isProjectMember(resource.data.projectId);
}
}
}
Teste no Emulator. Se no futuro você criar outro events para emails ou billing, a mesma collection group query pode alcançá-lo. Nomes como projectEvents e billingEvents deixam a segurança mais clara.
Revisão local com Claude Code
Antes de implementar, eu coloco docs/firestore-schema.md, firestore.rules, firestore.indexes.json e as query functions no repo.
claude -p "
Revise localmente este design Firestore.
Arquivos:
- docs/firestore-schema.md
- firestore.rules
- firestore.indexes.json
- src/lib/firestore/queries.ts
Verifique:
1. Cada query de tela combina com o schema?
2. Security Rules estão sendo usadas como filtros por engano?
3. As queries list têm where/orderBy/limit necessários?
4. Há composite indexes faltando ou sobrando?
5. Alguma collection group query é ampla demais?
6. O cliente consegue adulterar subscription status?
7. Qual tela lê documents demais?
Retorne problema, motivo e código corrigido.
"
Exemplo 3: assinaturas. Não permita escrita do cliente em subscriptions/{uid}; um webhook ou Cloud Function deve controlar esse document.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /subscriptions/{uid} {
allow get: if request.auth != null && request.auth.uid == uid;
allow list: if false;
allow create, update, delete: if false;
}
}
}
Também valide no servidor. Esconder botão na UI não é autorização.
import { getFirestore } from "firebase-admin/firestore";
const db = getFirestore();
export async function assertActiveSubscription(uid: string) {
const snap = await db.collection("subscriptions").doc(uid).get();
const data = snap.data();
if (!data || !["trialing", "active"].includes(data.status)) {
throw new Error("Active subscription required");
}
return data;
}
Minhas falhas recorrentes: IDs sequenciais, revisar rules sem revisar queries, misturar billing em users/{uid} e usar events para conceitos diferentes. Testei esse fluxo em gestão de contatos, admin editorial e demo SaaS; começar pela tabela de queries reduziu bastante o retrabalho.
Para continuar em GCP, leia Claude Code x GCP Cloud Functions e Claude Code x GCP Cloud Run. Se a fronteira da API ainda está confusa, design de REST API com Claude Code combina com este guia. O ClaudeCodeLab está transformando esses padrões em PDFs gratuitos, materiais e sessões de revisão; com schema, rules e lista de queries, a conversa vira ação concreta.
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.