Use Cases (Diperbarui: 1/6/2026)

Pengembangan Firebase dengan Claude Code: Auth, Rules, Functions, Hosting

Bangun Firebase dengan Claude Code: Auth, Firestore Rules, Functions, Hosting, Emulator Suite, pemisahan environment, biaya, dan keamanan.

Pengembangan Firebase dengan Claude Code: Auth, Rules, Functions, Hosting

Hal yang harus diputuskan sebelum meminta Claude Code membangun Firebase

Firebase adalah BaaS yang praktis untuk tim kecil yang ingin memiliki authentication, database, serverless functions, dan hosting tanpa mengelola backend penuh. Stack yang sering dipakai adalah Firebase Authentication, Cloud Firestore, Cloud Functions for Firebase, Firebase Hosting, dan Local Emulator Suite.

Risikonya: aplikasi Firebase bisa terlihat selesai tetapi belum aman. Firestore Security Rules yang terlalu longgar dapat membuka data pengguna lain. Admin SDK di Cloud Functions melewati Security Rules, jadi fungsi server harus memiliki validasi sendiri. Jika project development dan production tertukar, test lokal bisa menulis data nyata dan menimbulkan biaya nyata.

Saat memakai Claude Code, berikan tugas dalam satu fitur vertikal, bukan hanya satu layar. Misalnya fitur support ticket harus mencakup UI, status login, tulis Firestore, Security Rules, test emulator, Cloud Function untuk perubahan privileged, dan konfigurasi Hosting.

Untuk fondasi terkait, baca panduan implementasi authentication, panduan CI/CD, dan integrasi Supabase jika ingin membandingkan pilihan backend.

flowchart LR
  A["User"] --> B["Firebase Authentication"]
  B --> C["React or Astro UI"]
  C --> D["Cloud Firestore"]
  D --> E["Security Rules"]
  D --> F["Cloud Functions v2"]
  C --> G["Firebase Hosting"]
  H["Emulator Suite"] --> B
  H --> D
  H --> F

Contoh memakai Vite + React + TypeScript agar mudah disalin dan diuji. Di Next.js atau Astro, pola tetap sama, tetapi nama environment variable dan routing perlu disesuaikan.

Arsitektur minimum dan dokumentasi resmi

Gunakan dokumentasi resmi sebagai sumber utama. Di Firebase, snippet lama bisa tetap compile tetapi belum tentu aman untuk rules, emulator, dan deployment saat ini.

AreaLink resmiBatas tugas yang cocok untuk Claude Code
AuthenticationFirebase AuthenticationUI login, profil pengguna, auth state
FirestoreCloud FirestoreDesain collection, query, index
Security RulesFirestore Security RulesRules, test gagal, owner check
Cloud FunctionsCloud Functions for FirebaseValidasi server, notifikasi, agregasi
HostingFirebase HostingDeploy SPA, cache, preview channel
EmulatorLocal Emulator SuiteTest lokal, rule coverage, CI
PricingFirebase pricingReads, writes, function calls, logs
Claude CodeClaude Code docsUkuran tugas, review, eksekusi test

Struktur project yang mudah direview:

.
├─ firebase.json
├─ firestore.rules
├─ firestore.indexes.json
├─ .firebaserc
├─ functions/
│  ├─ package.json
│  └─ src/index.ts
└─ src/
   ├─ lib/firebase.ts
   ├─ lib/tickets.ts
   └─ lib/useAuth.tsx

Pisahkan environment sejak awal

Banyak masalah Firebase terjadi karena project tertukar. Tetapkan dev, staging, dan production sebelum meminta Claude Code menulis fitur.

{
  "projects": {
    "dev": "claudecodelab-firebase-dev",
    "stg": "claudecodelab-firebase-stg",
    "prod": "claudecodelab-firebase-prod"
  }
}

firebase.json menyimpan rules, indexes, Functions, Hosting, dan emulator.

{
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "functions": [
    {
      "source": "functions",
      "codebase": "default",
      "runtime": "nodejs20"
    }
  ],
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "headers": [
      {
        "source": "/assets/**",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "public, max-age=31536000, immutable"
          }
        ]
      }
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  },
  "emulators": {
    "auth": {
      "port": 9099
    },
    "functions": {
      "port": 5001
    },
    "firestore": {
      "port": 8080
    },
    "hosting": {
      "port": 5000
    },
    "ui": {
      "enabled": true,
      "port": 4000
    },
    "singleProjectMode": true
  }
}

Untuk Vite, .env.local berisi konfigurasi publik Web SDK. Firebase Web API key bukan rahasia server, tetapi service account JSON dan Admin SDK credential adalah rahasia.

VITE_FIREBASE_API_KEY=replace-me
VITE_FIREBASE_AUTH_DOMAIN=claudecodelab-firebase-dev.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=claudecodelab-firebase-dev
VITE_FIREBASE_STORAGE_BUCKET=claudecodelab-firebase-dev.appspot.com
VITE_FIREBASE_APP_ID=replace-me
VITE_USE_FIREBASE_EMULATORS=true

Authentication dan inisialisasi client

File ini menginisialisasi App, Auth, Firestore, dan Functions, lalu menyambungkan emulator saat development.

// src/lib/firebase.ts
import { initializeApp, getApp, getApps } from "firebase/app";
import {
  connectAuthEmulator,
  getAuth,
  GoogleAuthProvider,
} from "firebase/auth";
import { connectFirestoreEmulator, getFirestore } from "firebase/firestore";
import { connectFunctionsEmulator, getFunctions } from "firebase/functions";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
};

const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig);

export const auth = getAuth(app);
export const db = getFirestore(app);
export const functions = getFunctions(app, "asia-northeast1");
export const googleProvider = new GoogleAuthProvider();

const shouldUseEmulators =
  import.meta.env.DEV && import.meta.env.VITE_USE_FIREBASE_EMULATORS === "true";

const globalState = globalThis as typeof globalThis & {
  __firebaseEmulatorsConnected?: boolean;
};

if (shouldUseEmulators && !globalState.__firebaseEmulatorsConnected) {
  connectAuthEmulator(auth, "http://127.0.0.1:9099", {
    disableWarnings: true,
  });
  connectFirestoreEmulator(db, "127.0.0.1", 8080);
  connectFunctionsEmulator(functions, "127.0.0.1", 5001);
  globalState.__firebaseEmulatorsConnected = true;
}

Auth state lebih rapi jika dibungkus dalam Hook. Saat login pertama, buat atau update users/{uid} dengan merge.

// src/lib/useAuth.tsx
import { useEffect, useState } from "react";
import {
  onAuthStateChanged,
  signInWithPopup,
  signOut,
  type User,
} from "firebase/auth";
import { doc, serverTimestamp, setDoc } from "firebase/firestore";
import { auth, db, googleProvider } from "./firebase";

type AuthState = {
  user: User | null;
  loading: boolean;
  signInWithGoogle: () => Promise<void>;
  logout: () => Promise<void>;
};

export function useAuth(): AuthState {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    return onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser);
      setLoading(false);
    });
  }, []);

  async function signInWithGoogle() {
    const result = await signInWithPopup(auth, googleProvider);
    await setDoc(
      doc(db, "users", result.user.uid),
      {
        uid: result.user.uid,
        email: result.user.email,
        displayName: result.user.displayName,
        photoURL: result.user.photoURL,
        updatedAt: serverTimestamp(),
      },
      { merge: true },
    );
  }

  return {
    user,
    loading,
    signInWithGoogle,
    logout: () => signOut(auth),
  };
}

Prompt yang lebih aman untuk Claude Code:

Implementasikan Google sign-in dengan Firebase Auth.
- Stack Vite + React + TypeScript
- Reuse src/lib/firebase.ts yang sudah ada
- Saat login pertama, merge users/{uid}
- Return fungsi logout
- Jangan membuat, menampilkan, atau menyimpan credential service account
- Setelah selesai, jelaskan error type, dampak pada Firestore Rules, dan manual check

Model data Firestore dan CRUD

Contoh fitur adalah support ticket. Ada tiga use case nyata: portal member yang menampilkan ticket milik sendiri, workflow support yang menutup ticket lewat Function, dan dashboard internal yang membuat notifikasi saat ticket baru masuk.

// src/lib/tickets.ts
import {
  addDoc,
  collection,
  getDocs,
  limit,
  orderBy,
  query,
  serverTimestamp,
  where,
  type Timestamp,
} from "firebase/firestore";
import { db } from "./firebase";

export type TicketStatus = "open" | "closed";

export type Ticket = {
  id: string;
  userId: string;
  title: string;
  body: string;
  status: TicketStatus;
  createdAt: Timestamp;
  updatedAt: Timestamp;
};

type CreateTicketInput = {
  userId: string;
  title: string;
  body: string;
};

export async function createTicket(input: CreateTicketInput): Promise<string> {
  const title = input.title.trim();
  const body = input.body.trim();

  if (title.length === 0 || title.length > 120) {
    throw new Error("Title must be between 1 and 120 characters.");
  }

  if (body.length === 0 || body.length > 4000) {
    throw new Error("Body must be between 1 and 4000 characters.");
  }

  const docRef = await addDoc(collection(db, "tickets"), {
    userId: input.userId,
    title,
    body,
    status: "open",
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp(),
  });

  return docRef.id;
}

export async function listMyTickets(userId: string): Promise<Ticket[]> {
  const ticketsQuery = query(
    collection(db, "tickets"),
    where("userId", "==", userId),
    orderBy("createdAt", "desc"),
    limit(20),
  );

  const snapshot = await getDocs(ticketsQuery);

  return snapshot.docs.map((ticketDoc) => ({
    id: ticketDoc.id,
    ...(ticketDoc.data() as Omit<Ticket, "id">),
  }));
}

Query ini mungkin membutuhkan composite index. Simpan index di repository.

{
  "indexes": [
    {
      "collectionGroup": "tickets",
      "queryScope": "COLLECTION",
      "fields": [
        {
          "fieldPath": "userId",
          "order": "ASCENDING"
        },
        {
          "fieldPath": "createdAt",
          "order": "DESCENDING"
        }
      ]
    }
  ],
  "fieldOverrides": []
}

Firestore Security Rules bukan filter

Security Rules tidak memfilter query besar menjadi dokumen yang aman. Query harus dibentuk agar semua hasil yang mungkin memang boleh diakses. Jadi membaca semua tickets lalu memfilter di React adalah pola yang salah.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function signedIn() {
      return request.auth != null;
    }

    function isOwner(userId) {
      return signedIn() && request.auth.uid == userId;
    }

    function ticketFieldsAreValid() {
      return request.resource.data.keys().hasOnly([
        "userId",
        "title",
        "body",
        "status",
        "createdAt",
        "updatedAt"
      ])
      && request.resource.data.title is string
      && request.resource.data.title.size() > 0
      && request.resource.data.title.size() <= 120
      && request.resource.data.body is string
      && request.resource.data.body.size() > 0
      && request.resource.data.body.size() <= 4000;
    }

    match /users/{userId} {
      allow create, read, update: if isOwner(userId);
      allow delete: if false;
    }

    match /tickets/{ticketId} {
      allow create: if signedIn()
        && request.resource.data.userId == request.auth.uid
        && request.resource.data.status == "open"
        && ticketFieldsAreValid();

      allow read: if signedIn()
        && resource.data.userId == request.auth.uid;

      allow update: if signedIn()
        && resource.data.userId == request.auth.uid
        && request.resource.data.userId == resource.data.userId
        && request.resource.data.status == resource.data.status
        && request.resource.data.diff(resource.data).affectedKeys()
          .hasOnly(["title", "body", "updatedAt"])
        && ticketFieldsAreValid();

      allow delete: if false;
    }

    match /adminStats/{docId} {
      allow read, write: if false;
    }
  }
}

Review bagian request.auth.uid dibanding owner, daftar field yang dibatasi, delete yang ditolak, dan data admin yang tidak dibuka ke client.

Test rules dengan Emulator Suite

npm install -D vitest @firebase/rules-unit-testing firebase
firebase setup:emulators:firestore
// tests/firestore.rules.test.ts
import { readFileSync } from "node:fs";
import {
  assertFails,
  assertSucceeds,
  initializeTestEnvironment,
  type RulesTestEnvironment,
} from "@firebase/rules-unit-testing";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { doc, getDoc, setDoc, updateDoc } from "firebase/firestore";

let testEnv: RulesTestEnvironment;

beforeAll(async () => {
  testEnv = await initializeTestEnvironment({
    projectId: "claudecodelab-firestore-rules",
    firestore: {
      rules: readFileSync("firestore.rules", "utf8"),
    },
  });
});

beforeEach(async () => {
  await testEnv.clearFirestore();
});

afterAll(async () => {
  await testEnv.cleanup();
});

describe("tickets security rules", () => {
  it("allows the owner to create and read a ticket", async () => {
    const aliceDb = testEnv.authenticatedContext("alice").firestore();
    const ticketRef = doc(aliceDb, "tickets/ticket-1");

    await assertSucceeds(
      setDoc(ticketRef, {
        userId: "alice",
        title: "Please resend my invoice",
        body: "I cannot find the April invoice.",
        status: "open",
        createdAt: new Date(),
        updatedAt: new Date(),
      }),
    );

    await assertSucceeds(getDoc(ticketRef));
  });

  it("blocks another user from reading the ticket", async () => {
    await testEnv.withSecurityRulesDisabled(async (context) => {
      await setDoc(doc(context.firestore(), "tickets/ticket-2"), {
        userId: "alice",
        title: "Plan question",
        body: "I want to confirm my current plan.",
        status: "open",
        createdAt: new Date(),
        updatedAt: new Date(),
      });
    });

    const bobDb = testEnv.authenticatedContext("bob").firestore();
    await assertFails(getDoc(doc(bobDb, "tickets/ticket-2")));
  });

  it("blocks status changes from the web client", async () => {
    await testEnv.withSecurityRulesDisabled(async (context) => {
      await setDoc(doc(context.firestore(), "tickets/ticket-3"), {
        userId: "alice",
        title: "Cannot sign in",
        body: "Google sign-in returns an error.",
        status: "open",
        createdAt: new Date(),
        updatedAt: new Date(),
      });
    });

    const aliceDb = testEnv.authenticatedContext("alice").firestore();
    await assertFails(
      updateDoc(doc(aliceDb, "tickets/ticket-3"), {
        status: "closed",
        updatedAt: new Date(),
      }),
    );
  });

  it("keeps the test runner alive", () => {
    expect(testEnv).toBeDefined();
  });
});
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "firebase:use:dev": "firebase use dev",
    "firebase:emulators": "firebase emulators:start --only auth,firestore,functions,hosting",
    "test:rules": "firebase emulators:exec --only firestore \"vitest run tests/firestore.rules.test.ts\"",
    "deploy:stg": "firebase use stg && npm run build && firebase deploy --only hosting,firestore:rules,firestore:indexes,functions",
    "deploy:prod": "firebase use prod && npm run build && firebase deploy --only hosting,firestore:rules,firestore:indexes,functions"
  }
}

Pindahkan operasi privileged ke Cloud Functions

Perubahan status, external API key, notifikasi, dan agregasi tidak boleh bergantung pada browser.

// functions/src/index.ts
import { initializeApp } from "firebase-admin/app";
import { FieldValue, getFirestore } from "firebase-admin/firestore";
import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { HttpsError, onCall } from "firebase-functions/v2/https";

initializeApp();

const db = getFirestore();

export const closeTicket = onCall(
  {
    region: "asia-northeast1",
  },
  async (request) => {
    if (!request.auth) {
      throw new HttpsError("unauthenticated", "Sign-in is required.");
    }

    const ticketId = request.data?.ticketId;
    if (typeof ticketId !== "string" || ticketId.length > 100) {
      throw new HttpsError("invalid-argument", "ticketId is invalid.");
    }

    const ticketRef = db.doc(`tickets/${ticketId}`);
    const ticketSnap = await ticketRef.get();

    if (!ticketSnap.exists) {
      throw new HttpsError("not-found", "Ticket was not found.");
    }

    const ticket = ticketSnap.data();
    if (ticket?.userId !== request.auth.uid) {
      throw new HttpsError("permission-denied", "You cannot close this ticket.");
    }

    await ticketRef.update({
      status: "closed",
      closedAt: FieldValue.serverTimestamp(),
      updatedAt: FieldValue.serverTimestamp(),
    });

    return { ok: true };
  },
);

export const notifyTicketCreated = onDocumentCreated(
  {
    document: "tickets/{ticketId}",
    region: "asia-northeast1",
  },
  async (event) => {
    const ticket = event.data?.data();
    if (!ticket) return;

    await db.collection("adminNotifications").add({
      type: "ticket_created",
      ticketId: event.params.ticketId,
      title: ticket.title,
      userId: ticket.userId,
      createdAt: FieldValue.serverTimestamp(),
      read: false,
    });
  },
);

Poin penting: Admin SDK melewati Security Rules. Karena itu Callable Function harus memeriksa request.auth, tipe input, dan owner document.

Hosting, biaya, dan review keamanan

firebase login
firebase use dev
npm run build
firebase emulators:start --only auth,firestore,functions,hosting
firebase hosting:channel:deploy preview-firebase-ticket
firebase use stg
firebase deploy --only hosting,firestore:rules,firestore:indexes,functions

Kesalahan umum: semua user login boleh read/write; seluruh collection dibaca lalu difilter di React; Functions tidak memeriksa owner; emulator tidak memuat firestore.rules yang sama; atau .env.local, firebase use, dan Firebase Console menunjuk project berbeda.

Untuk biaya, cek angka terbaru di Firebase pricing. Review Firestore reads, limit, cleanup realtime listener, region dan timeout Functions, volume log, dan cache Hosting. Untuk keamanan, jangan masukkan service account JSON, token CI, atau role Owner production ke prompt Claude Code.

Template prompt untuk Claude Code

Implementasikan fitur Firebase support ticket.

Scope:
- Vite + React + TypeScript
- Firebase Auth, Firestore, Cloud Functions v2, Hosting
- Hanya edit src/lib/firebase.ts, src/lib/useAuth.tsx, src/lib/tickets.ts, firestore.rules, functions/src/index.ts, firebase.json

Requirements:
- User Google login dapat membuat tickets
- User hanya dapat membaca tickets miliknya
- Web client tidak dapat mengubah status
- Callable Function menutup ticket setelah validasi owner
- Tambahkan Emulator Suite tests untuk akses allowed dan denied
- Pisahkan dev/stg/prod

Forbidden:
- Membuat, mencetak, atau menyimpan service account JSON
- Menjalankan production deploy
- Menggunakan allow read, write: if true

Report:
- Changed files
- Tests run
- Permission boundary di Security Rules
- Manual checks yang tersisa

Kesimpulan

Claude Code cocok untuk Firebase karena rules, functions, indexes, hosting config, dan client SDK calls menghasilkan diff yang bisa direview. Namun keputusan permission, production deploy, ownership data, dan batas biaya tetap harus dikendalikan manusia.

Dalam project nyata, bangun satu fitur kecil di Emulator Suite, tambahkan negative tests untuk Security Rules, review authorization di Cloud Functions, lalu deploy lewat preview channel sebelum staging dan production. ClaudeCodeLab dapat membantu implementasi Firebase, review Security Rules, training Emulator Suite, dan workflow tim dengan Claude Code.

Saat mencoba isi artikel ini, pastikan Anda memakai Firebase project development, firebase use cocok dengan .env.local, test rules untuk kasus sukses dan gagal sama-sama pass, Cloud Functions memvalidasi owner secara internal, dan Claude Code tidak diberi izin menjalankan production deploy otomatis.

#Claude Code #Firebase #Firestore #Cloud Functions #BaaS
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.