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.
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.
| Area | Link resmi | Batas tugas yang cocok untuk Claude Code |
|---|---|---|
| Authentication | Firebase Authentication | UI login, profil pengguna, auth state |
| Firestore | Cloud Firestore | Desain collection, query, index |
| Security Rules | Firestore Security Rules | Rules, test gagal, owner check |
| Cloud Functions | Cloud Functions for Firebase | Validasi server, notifikasi, agregasi |
| Hosting | Firebase Hosting | Deploy SPA, cache, preview channel |
| Emulator | Local Emulator Suite | Test lokal, rule coverage, CI |
| Pricing | Firebase pricing | Reads, writes, function calls, logs |
| Claude Code | Claude Code docs | Ukuran 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.
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.