Desain Schema Firestore dengan Claude Code: Panduan GCP/Firebase untuk SaaS
Rancang collection, rules, index, dan schema SaaS Firestore dengan Claude Code tanpa jebakan keamanan umum.
Desain Firestore dimulai dari cara membaca data
Saya Masa, pengelola claudecode-lab.com.
Kesalahan pertama saya saat memakai Firestore adalah langsung memberi nama collection: users, projects, events, subscriptions. Di awal terlihat rapi. Namun ketika aplikasi SaaS mulai punya dashboard proyek, daftar member, event terbaru, status trial, billing, dan halaman admin, schema yang rapi tadi tidak lagi cocok dengan query yang dibutuhkan layar.
Firestore bukan database relasional yang bisa diperbaiki belakangan dengan JOIN. Dokumentasi resmi tentang data model menjelaskan bahwa data disimpan sebagai document di dalam collection. Document bisa berisi field, object, dan subcollection. Analogi sederhana: collection adalah rak, document adalah map, subcollection adalah map kecil di dalam map utama.
Urutan yang lebih aman adalah: layar, query, schema, Security Rules, lalu index. Claude Code paling berguna sebagai reviewer lokal. Saya tidak hanya meminta “buatkan schema”, tetapi meminta Claude membandingkan firestore.rules, firestore.indexes.json, tipe TypeScript, dan fungsi query untuk mencari kontradiksi.
Collection, document, dan subcollection untuk SaaS
SaaS B2B kecil bisa mulai dari struktur ini:
users/{uid}
projects/{projectId}
projects/{projectId}/members/{uid}
projects/{projectId}/events/{eventId}
subscriptions/{uid}
billingCustomers/{uid}
| Path | Peran | Bacaan umum |
|---|---|---|
users/{uid} | Profil pengguna | Profil sendiri |
projects/{projectId} | Workspace atau proyek pelanggan | Detail proyek |
projects/{projectId}/members/{uid} | Role dalam proyek | Daftar member dan izin |
projects/{projectId}/events/{eventId} | Aktivitas dan audit | Event terbaru proyek |
subscriptions/{uid} | Plan dan status billing | Batasan fitur |
billingCustomers/{uid} | ID Stripe atau provider billing | Job server saja |
Subcollection dipakai karena cocok dengan pola baca yang sering. Jika layar utama membaca 50 event terbaru dari satu proyek, projects/{projectId}/events masuk akal. Jika nanti perlu membaca event lintas proyek, collection group query bisa digunakan, tetapi rules dan index harus ikut dirancang.
Prompt yang saya pakai:
claude -p "
Review desain Firestore untuk SaaS B2B.
Jangan usulkan collection dulu. Buat inventaris query per layar.
Layar:
- Proyek milik user saat ini
- Detail proyek
- 50 event terbaru dalam proyek
- Daftar admin status subscription
- User yang trial-nya akan berakhir
Untuk tiap layar, tulis where/orderBy/limit,
Composite index yang diperlukan, dan kondisi Security Rules.
"
Prompt ini membuat Claude Code berpikir dari perilaku produk, bukan dari diagram data yang terlihat rapi.
Contoh schema SaaS untuk user, project, event, dan billing
Model berikut cocok untuk Firebase Admin SDK atau Cloud Functions. Walaupun sebagian write dilakukan dari client, tipe ini membuat kontrak data lebih jelas.
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 dan email di ProjectMemberDoc adalah denormalisasi yang disengaja. Denormalisasi berarti menyalin sedikit data tampilan agar tidak perlu membaca banyak document tambahan. Di Firestore, membaca 50 users/{uid} hanya untuk menampilkan nama member bisa mahal. Menyimpan nama di document membership membuat daftar member lebih cepat, dengan konsekuensi harus disinkronkan saat profil berubah.
Contoh 1: untuk dashboard awal, saya sering menambahkan referensi proyek per user.
users/{uid}/projectRefs/{projectId}
projectId: string
projectName: string
role: "owner" | "admin" | "member" | "viewer"
lastEventAt: Timestamp | null
Ini bukan model paling murni, tetapi layar pertama menjadi satu pola baca yang stabil.
Security Rules bukan filter
Ini jebakan paling umum. Dokumentasi resmi tentang secure queries menyatakan bahwa rules bukan filter. Query diterima atau ditolak seluruhnya. Jika query mungkin mengembalikan document yang tidak boleh dibaca user, query gagal.
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;
}
}
}
Query berikut salah karena tidak memakai limit.
import { collection, getDocs } from "firebase/firestore";
await getDocs(collection(db, "projects", projectId, "events"));
Query harus cocok dengan rules.
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() }));
}
Contoh 2: jika rules hanya mengizinkan visibility == "public", query juga harus punya where("visibility", "==", "public"). Firestore tidak otomatis mengembalikan “yang boleh dilihat saja”.
Composite index dan collection group query
Firestore membuat index dasar secara otomatis, tetapi kombinasi filter dan sorting sering membutuhkan composite index. Dokumentasi resmi indexing menjelaskan bahwa query tanpa index bisa memberi link untuk membuat index. Dalam tim, saya lebih suka menyimpan firestore.indexes.json di 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": []
}
Collection group query membaca semua subcollection dengan ID yang sama.
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 harus mengikuti pola itu. Dokumentasi resmi struktur rules mengingatkan bahwa match menunjuk document path, bukan collection. Untuk collection group query, gunakan rules version 2 dan recursive wildcard.
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);
}
}
}
Uji dengan Emulator. Jika nanti ada collection lain bernama events untuk email atau billing, collection group query bisa ikut mengenainya. Nama yang lebih spesifik seperti projectEvents atau billingEvents lebih aman.
Review lokal dengan Claude Code
Sebelum implementasi jauh, saya taruh docs/firestore-schema.md, firestore.rules, firestore.indexes.json, dan query functions di repo.
claude -p "
Review desain Firestore ini secara lokal.
File:
- docs/firestore-schema.md
- firestore.rules
- firestore.indexes.json
- src/lib/firestore/queries.ts
Cek:
1. Apakah setiap query layar cocok dengan schema?
2. Apakah Security Rules keliru dipakai seperti filter?
3. Apakah list query punya where/orderBy/limit yang diperlukan?
4. Apakah Composite index kurang atau berlebihan?
5. Apakah collection group query terlalu luas?
6. Apakah client bisa mengubah subscription status?
7. Layar mana yang membaca terlalu banyak document?
Kembalikan masalah, alasan, dan kode perbaikan.
"
Contoh 3: subscription. Jangan izinkan client menulis subscriptions/{uid}. Webhook atau Cloud Function harus menjadi pemilik dokumen itu.
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;
}
}
}
Validasi juga di server. Menyembunyikan tombol di UI bukan otorisasi.
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;
}
Kegagalan yang sering saya lihat: document ID berurutan, rules dan query direview terpisah, status billing dicampur ke users/{uid}, dan semua log dinamai events. Saya mencoba alur ini pada manajemen kontak, admin konten, dan demo SaaS kecil; membuat tabel query di awal mengurangi banyak revisi.
Untuk lanjut ke GCP, baca Claude Code x GCP Cloud Functions dan Claude Code x GCP Cloud Run. Jika batas API masih kabur, desain REST API dengan Claude Code cocok sebagai lanjutan. ClaudeCodeLab juga menyiapkan PDF gratis, materi belajar, dan sesi konsultasi dari checklist ini; dengan schema, rules, dan daftar query, konsultasi bisa langsung masuk ke review konkret.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.