Firestore-Schema mit Claude Code entwerfen: GCP/Firebase SaaS Guide
Plane Firestore Collections, Rules, Indexes und SaaS-Datenmodelle mit Claude Code ohne Sicherheitsfallen.
Firestore-Design beginnt bei den Reads, nicht bei schönen Collection-Namen
Ich bin Masa, Betreiber von claudecode-lab.com.
Mein erster Firestore-Fehler war harmlos, aber teuer: Ich begann mit Collection-Namen. users, projects, events, subscriptions sahen sauber aus. Dann kamen die echten SaaS-Screens: Projektübersicht, Mitgliederliste, Audit-Events, Trial-Erinnerungen, Admin-Abrechnung und Feature-Gating nach Plan. Das Schema sah ordentlich aus, aber die Abfragen passten nicht zum Produkt.
Firestore ist keine relationale Datenbank, in der man später alles mit JOINs glättet. Das offizielle Datenmodell beschreibt Daten als Dokumente innerhalb von Collections. Ein Dokument kann Felder, verschachtelte Objekte und Subcollections enthalten. Einfach gesagt: Eine Collection ist ein Regal, ein Dokument ist eine Akte, und eine Subcollection ist ein kleiner Ordner in dieser Akte.
Der robuste Ablauf lautet deshalb: Screens, Queries, Schema, Security Rules, Indexes. Claude Code ist dafür ein guter lokaler Reviewer. Ich lasse es nicht nur Code erzeugen, sondern Widersprüche zwischen firestore.rules, firestore.indexes.json, TypeScript-Typen und Query-Funktionen suchen.
Collections, Documents und Subcollections für ein SaaS
Ein kleines B2B-SaaS kann mit dieser Struktur starten:
users/{uid}
projects/{projectId}
projects/{projectId}/members/{uid}
projects/{projectId}/events/{eventId}
subscriptions/{uid}
billingCustomers/{uid}
| Pfad | Zweck | Typischer Read |
|---|---|---|
users/{uid} | Profil des Users | Eigenes Profil |
projects/{projectId} | Workspace oder Kundenprojekt | Projektdetail |
projects/{projectId}/members/{uid} | Rolle im Projekt | Mitgliederliste, Zugriff |
projects/{projectId}/events/{eventId} | Aktivität, Audit, Benachrichtigung | Letzte Projekt-Events |
subscriptions/{uid} | Plan und Zahlungsstatus | Feature-Gating |
billingCustomers/{uid} | Stripe- oder Billing-IDs | Nur Server-Jobs |
Subcollections sollten nicht aus Gewohnheit entstehen. Sie sollten zu einem häufigen Read passen. Wenn der wichtigste Screen die letzten 50 Events eines Projekts zeigt, ist projects/{projectId}/events sinnvoll. Wenn du später projektübergreifend suchen musst, kann eine Collection Group Query helfen, aber sie hat Folgen für Rules und Indexes.
So frage ich Claude Code:
claude -p "
Reviewe das Firestore-Design eines B2B-SaaS.
Schlage noch keine Collections vor. Erstelle zuerst ein Query-Inventar pro Screen.
Screens:
- Projekte des aktuellen Users
- Projektdetail
- Letzte 50 Events in einem Projekt
- Admin-Liste der Subscription-Status
- User, deren Trial bald endet
Gib pro Screen where/orderBy/limit, notwendige Composite Indexes
und die passende Security-Rules-Bedingung zurück.
"
Damit denkt Claude Code vom Produkt aus und nicht von einem hübschen Diagramm.
Ein praktikables SaaS-Schema
Dieses Modell ist für Firebase Admin SDK oder Cloud Functions gedacht. Auch wenn einige Writes direkt vom Client kommen, geben diese Typen einen klaren Vertrag vor.
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 und email im Membership-Dokument sind absichtliche Denormalisierung. Denormalisierung bedeutet, kleine Anzeigedaten zu kopieren, damit eine Liste nicht dutzende zusätzliche Dokumente lesen muss. In Firestore ist das oft die bessere Wahl: 50 Mitglieder plus 50 users/{uid} Reads sind teuer und langsam. Du synchronisierst die kopierten Felder bei Profiländerungen und bekommst dafür schnelle Listen.
Beispiel 1: Für das Home-Dashboard nutze ich häufig eine Referenzsammlung pro User.
users/{uid}/projectRefs/{projectId}
projectId: string
projectName: string
role: "owner" | "admin" | "member" | "viewer"
lastEventAt: Timestamp | null
Das ist nicht akademisch rein, aber der erste Screen wird damit ein vorhersehbarer Read.
Security Rules sind keine Filter
Das ist die häufigste Falle. Die offizielle Doku zu sicheren Queries sagt: Security Rules filtern keine Ergebnisse. Eine Query wird vollständig akzeptiert oder abgelehnt. Wenn sie ein verbotenes Dokument zurückgeben könnte, schlägt sie fehl.
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;
}
}
}
Diese Query passt nicht, weil die Rule ein Limit verlangt:
import { collection, getDocs } from "firebase/firestore";
await getDocs(collection(db, "projects", projectId, "events"));
So wird sie regelkonform:
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() }));
}
Beispiel 2: Wenn eine Rule nur visibility == "public" erlaubt, muss die Query auch where("visibility", "==", "public") enthalten. Firestore gibt nicht automatisch nur sichtbare Dokumente zurück.
Composite Indexes und Collection Group Queries
Firestore erstellt einfache Indexes automatisch, aber Kombinationen aus Filtern und Sortierung brauchen oft Composite Indexes. Die offizielle Seite zur Indexverwaltung erklärt, dass fehlende Indexes über einen Fehlerlink erstellt werden können. Im Team gehört firestore.indexes.json trotzdem in 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": []
}
Eine Collection Group Query liest alle Subcollections mit demselben Namen. Für Projekt-Events sieht das so aus:
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() }));
}
Rules müssen das abdecken. Der offizielle Guide zur Rules-Struktur erklärt, dass match Dokumentpfade beschreibt, nicht Collections. Für Collection Group Queries nutzt man Version 2 und ein rekursives Wildcard-Match.
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 das im Emulator. Wenn du später eine zweite Collection namens events für Billing oder E-Mail-Logs anlegst, wird die Collection Group Query auch diese treffen. Präzisere Namen wie projectEvents oder billingEvents sind oft sicherer.
Lokale Review mit Claude Code
Vor der Implementierung lege ich docs/firestore-schema.md, firestore.rules, firestore.indexes.json und die Query-Funktionen ins Repo.
claude -p "
Reviewe dieses Firestore-Design lokal.
Dateien:
- docs/firestore-schema.md
- firestore.rules
- firestore.indexes.json
- src/lib/firestore/queries.ts
Prüfe:
1. Passt jede Screen-Query zum Schema?
2. Werden Security Rules fälschlich als Filter benutzt?
3. Haben alle list-Queries die nötigen where/orderBy/limit?
4. Fehlen Composite Indexes oder sind welche überflüssig?
5. Ist eine Collection Group Query zu breit?
6. Kann der Client den Subscription-Status manipulieren?
7. Welcher Screen liest zu viele Dokumente?
Gib Problem, Grund und korrigierten Code zurück.
"
Beispiel 3: Subscriptions. Clients sollten subscriptions/{uid} nicht schreiben. Ein Stripe-Webhook oder eine Cloud Function sollte Eigentümer dieses Dokuments sein.
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;
}
}
}
Prüfe den Plan zusätzlich serverseitig. Ein versteckter Button ist keine Autorisierung.
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;
}
Meine typischen Fehler: sequentielle IDs, Rules getrennt von Queries prüfen, Billing-Status in users/{uid} mischen, und events für zu viele Konzepte verwenden. Bei einem Kontaktmanager, einem Content-Admin und einer SaaS-Demo hatte die Query-Tabelle am Anfang den größten Effekt.
Für mehr GCP-Kontext lies Claude Code x GCP Cloud Functions und Claude Code x GCP Cloud Run. Wenn deine API-Grenzen noch unscharf sind, passt REST API Design mit Claude Code dazu. ClaudeCodeLab bereitet daraus kostenlose PDFs, Lernmaterialien und Review-Sessions vor; mit Schema, Rules und Query-Liste wird eine Beratung sofort konkret.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.