Diseño de esquemas Firestore con Claude Code: guía SaaS para GCP/Firebase
Diseña colecciones, reglas, índices y modelos SaaS de Firestore con Claude Code y evita errores de seguridad.
Firestore se diseña desde las lecturas, no desde nombres bonitos
Soy Masa, operador de claudecode-lab.com.
Mi primer error con Firestore fue empezar por nombres de colecciones: users, projects, events, subscriptions. En una pizarra se veía ordenado, pero el producto SaaS real pidió otra cosa: listar proyectos del usuario, mostrar miembros con roles, leer los últimos eventos, bloquear funciones por plan, revisar trials que terminan pronto y proteger todo con Security Rules. La estructura parecía limpia, pero las consultas no salían limpias.
Firestore no es una base relacional donde luego arreglas el modelo con JOIN. La documentación oficial del modelo de datos explica que los datos viven en documentos dentro de colecciones. Un documento puede tener campos, objetos anidados y subcolecciones. Una forma sencilla de verlo: la colección es una estantería, el documento es una carpeta, y la subcolección es una carpeta pequeña dentro de esa carpeta.
Por eso el flujo sano es: primero pantallas y consultas; después schema; luego rules e índices. Claude Code funciona muy bien como revisor local de contradicciones. No le pido que invente la arquitectura; le pido que compare schema, queries, firestore.rules y firestore.indexes.json antes de que el error llegue a producción.
Colecciones, documentos y subcolecciones en un SaaS
Un SaaS B2B pequeño puede empezar con este mapa:
users/{uid}
projects/{projectId}
projects/{projectId}/members/{uid}
projects/{projectId}/events/{eventId}
subscriptions/{uid}
billingCustomers/{uid}
| Ruta | Uso | Lectura típica |
|---|---|---|
users/{uid} | Perfil del usuario | Perfil propio |
projects/{projectId} | Workspace o proyecto del cliente | Detalle de proyecto |
projects/{projectId}/members/{uid} | Rol dentro del proyecto | Lista de miembros y permisos |
projects/{projectId}/events/{eventId} | Auditoría, actividad, notificaciones | Últimos eventos del proyecto |
subscriptions/{uid} | Plan y estado de pago | Control de funciones |
billingCustomers/{uid} | IDs de Stripe u otro proveedor | Procesos de servidor |
La decisión importante es dónde se leerá con más frecuencia. Si la pantalla principal de eventos siempre está dentro de un proyecto, la subcolección projects/{projectId}/events es natural. Si después necesitas una vista cruzada, puedes usar collection group query, pero eso cambia reglas e índices.
Prompt útil para Claude Code:
claude -p "
Revisa el diseño Firestore de un SaaS B2B.
Antes de proponer colecciones, crea un inventario de consultas por pantalla.
Pantallas:
- Proyectos del usuario actual
- Detalle de proyecto
- Últimos 50 eventos del proyecto
- Lista admin de estados de suscripción
- Usuarios cuyo trial termina pronto
Devuelve where/orderBy/limit, índices compuestos necesarios
y condición de Security Rules para cada pantalla.
"
Así Claude razona desde el producto, no desde una estructura abstracta.
Schema práctico para usuarios, proyectos, eventos y pagos
Este modelo está pensado para Firebase Admin SDK o Cloud Functions. Aunque algunas escrituras salgan desde el cliente, estas interfaces ayudan a fijar el 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;
}
Guardar displayName y email dentro del documento de miembro es desnormalización deliberada. En Firestore, leer 50 usuarios adicionales solo para pintar una lista de miembros puede ser caro y lento. Copiar campos pequeños reduce lecturas. El coste es sincronizar esos campos cuando cambia el perfil.
Ejemplo 1: para el dashboard inicial, uso una colección de referencias por usuario.
users/{uid}/projectRefs/{projectId}
projectId: string
projectName: string
role: "owner" | "admin" | "member" | "viewer"
lastEventAt: Timestamp | null
No es el modelo más puro, pero convierte la primera pantalla en una lectura predecible.
Security Rules no son filtros
Este es el tropiezo más serio. La guía oficial de consultas seguras dice que las reglas no filtran resultados; la consulta se acepta o se rechaza completa. Si una consulta podría devolver un documento no permitido, falla.
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;
}
}
}
La consulta siguiente no cumple la regla porque no tiene limit.
import { collection, getDocs } from "firebase/firestore";
await getDocs(collection(db, "projects", projectId, "events"));
Versión correcta:
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() }));
}
Ejemplo 2: si la regla permite leer solo visibility == "public", la consulta también debe usar where("visibility", "==", "public"). Firestore no devuelve “solo lo visible”; exige que la consulta demuestre que todo lo posible es visible.
Índices compuestos y collection group query
Firestore crea índices básicos, pero las combinaciones de filtros y orden suelen requerir composite index. La página oficial de gestión de índices explica que un índice faltante genera un enlace para crearlo. En equipo, prefiero guardar firestore.indexes.json en Git.
{
"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": []
}
Una collection group query lee todas las subcolecciones con el mismo ID. Sirve para cruzar projects/{projectId}/events cuando el producto lo necesita.
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() }));
}
Las reglas deben permitir ese patrón. La guía de estructura de reglas recuerda que match apunta a documentos, no a colecciones. Para collection group query usa versión 2 y 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);
}
}
}
Pruébalo en el Emulator. Si en el futuro creas otra colección llamada events para emails o facturación, la misma collection group puede tocarla. Nombres más específicos como projectEvents o billingEvents evitan confusión.
Revisión local con Claude Code
Prompt que uso antes de implementar de verdad:
claude -p "
Revisa localmente este diseño Firestore.
Archivos:
- docs/firestore-schema.md
- firestore.rules
- firestore.indexes.json
- src/lib/firestore/queries.ts
Comprueba:
1. Si cada consulta de pantalla coincide con el schema
2. Si alguna regla se usa por error como filtro
3. Si faltan where/orderBy/limit en consultas list
4. Si faltan o sobran índices compuestos
5. Si una collection group query es demasiado amplia
6. Si el cliente puede alterar el estado de suscripción
7. Qué pantalla lee demasiados documentos
Devuelve problema, motivo y código corregido.
"
Ejemplo 3: suscripciones. No permitas escrituras de cliente en subscriptions/{uid}; que escriba un webhook o una Cloud Function.
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;
}
}
}
Además, valida el plan en servidor. Ocultar botones en la UI no es autorización.
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;
}
Mis fallos recurrentes: IDs secuenciales, reglas y queries revisadas por separado, estado de pago mezclado en users/{uid}, y nombres genéricos como events usados para todo. Probé este flujo en gestión de contactos, panel editorial y un demo SaaS; empezar con la tabla de queries redujo bastante la reescritura.
Para continuar con GCP, lee Claude Code x GCP Cloud Functions y Claude Code x GCP Cloud Run. Si el límite de API todavía no está claro, diseño de REST API con Claude Code encaja con este tema. ClaudeCodeLab está convirtiendo estos patrones en PDF gratuitos, materiales y sesiones de revisión; si traes schema, rules y lista de queries, la consulta empieza en terreno concreto.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.