Use Cases (Diperbarui: 1/6/2026)

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 Schema Firestore dengan Claude Code: Panduan GCP/Firebase untuk SaaS

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}
PathPeranBacaan umum
users/{uid}Profil penggunaProfil sendiri
projects/{projectId}Workspace atau proyek pelangganDetail proyek
projects/{projectId}/members/{uid}Role dalam proyekDaftar member dan izin
projects/{projectId}/events/{eventId}Aktivitas dan auditEvent terbaru proyek
subscriptions/{uid}Plan dan status billingBatasan fitur
billingCustomers/{uid}ID Stripe atau provider billingJob 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.

#claude-code #gcp #firestore #database #typescript #query-design
Gratis

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.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.