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.
Artigos relacionados
GCP Cloud Functions com Claude Code: HTTP, Secret Manager e Cloud Logging
Crie Cloud Run functions com Claude Code: HTTP, segredos, logs, verificação de deploy e armadilhas.
Deploy no GCP Cloud Run com Claude Code: guia prático
Publique uma API Node.js segura no Cloud Run com Docker, IAM, secrets, logs e rollback.
Claude Code e AWS DynamoDB: design de tabela, escritas seguras e custos
Guia prático de Claude Code com DynamoDB: chaves, testes locais, TTL, IAM, custos e hot partitions.
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.