Use Cases (Atualizado: 01/06/2026)

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.

Schema Firestore com Claude Code: guia GCP/Firebase para SaaS

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}
CaminhoPapelLeitura típica
users/{uid}Perfil do usuárioPerfil atual
projects/{projectId}Workspace ou projetoDetalhe do projeto
projects/{projectId}/members/{uid}Role por projetoMembros e permissão
projects/{projectId}/events/{eventId}Atividade e auditoriaEventos recentes
subscriptions/{uid}Plano e status de cobrançaControle de recursos
billingCustomers/{uid}IDs de Stripe ou billingJobs 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.

#claude-code #gcp #firestore #database #typescript #query-design
Grátis

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.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.