api-baas-firebase
Firebase backend-as-a-service — Firestore, Authentication, Cloud Functions v2, Storage, Hosting, Admin SDK, security rules, emulator suite
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-baas-firebase/skills/api-baas-firebase
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Firebase Patterns
Quick Guide: Use Firebase as your backend-as-a-service for Firestore database, authentication, Cloud Functions, file storage, and hosting. Always use the modular SDK (
firebase/app,firebase/firestore, etc.) for tree-shaking, type Firestore documents with TypeScript interfaces, write security rules for every collection, and use the Admin SDK only on the server.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use the modular Firebase SDK imports (firebase/app, firebase/firestore, firebase/auth) -- NEVER use the deprecated firebase/compat namespace API)
(You MUST write Firestore security rules for EVERY collection -- a collection without rules is wide open in production)
(You MUST NEVER expose Firebase Admin SDK credentials or service account keys in client-side code)
(You MUST use Cloud Functions v2 API (firebase-functions/v2/https, firebase-functions/v2/firestore) -- NOT the deprecated v1 API)
(You MUST handle all Firestore operations with error checking -- never assume reads/writes succeed)
</critical_requirements>
Auto-detection: Firebase, initializeApp, firebase/app, firebase/firestore, firebase/auth, getFirestore, getAuth, onAuthStateChanged, collection, doc, getDocs, setDoc, updateDoc, deleteDoc, onSnapshot, firebase-admin, firebase-functions, Cloud Functions, Firestore security rules, firebase.json, firebase deploy
When to use:
- Initializing Firebase and configuring services (Firestore, Auth, Storage, Functions)
- Implementing authentication (email/password, OAuth, phone, custom tokens)
- Querying and writing Firestore documents (CRUD, real-time listeners, transactions)
- Writing Cloud Functions v2 (HTTP handlers, callable functions, Firestore triggers, scheduled functions)
- Uploading and serving files from Firebase Storage
- Deploying to Firebase Hosting with function rewrites
- Writing Firestore and Storage security rules
- Using the Firebase Admin SDK for server-side operations
Key patterns covered:
- Modular SDK setup with
initializeAppand service getters (getFirestore,getAuth,getStorage) - Auth flows: sign up, sign in, OAuth, phone auth,
onAuthStateChanged, session management - Firestore CRUD with
doc(),collection(),getDocs(),setDoc(),updateDoc(),deleteDoc() - Firestore real-time listeners with
onSnapshot() - Firestore queries with
where(),orderBy(),limit(), composite indexes - Cloud Functions v2:
onRequest,onCall,onDocumentCreated,onSchedule - Firebase Admin SDK:
initializeApp(),getFirestore(),getAuth(), custom tokens, user management - Security rules: read/write granularity, auth-based access, data validation
- Emulator suite for local development and testing
- Offline persistence with
persistentLocalCache
When NOT to use:
- Complex relational queries needing JOIN operations (use a relational database with an ORM)
- Full server-side ORM patterns (Firestore is a document database, not relational)
- Applications using a non-Firebase authentication provider
- Applications requiring complex server-side business logic beyond Cloud Functions scope
Examples:
- Core Setup & Configuration -- App init, emulators, offline persistence
- Firestore Database -- CRUD, queries, real-time listeners, transactions
- Authentication -- Email/password, OAuth, auth state, profile sync
- Cloud Functions & Admin SDK -- HTTP, callable, triggers, scheduled, Admin SDK
- Cloud Storage -- Upload with progress, validation, App Check
- Security Rules -- Firestore and Storage rules patterns
<decision_framework>
Decision Framework
Firestore vs Realtime Database
What does your app need?
+-- Complex queries (where, orderBy, compound) --> Firestore
+-- Simple key-value lookups with low latency --> Realtime Database
+-- Offline support with rich queries --> Firestore
+-- Presence system (online/offline status) --> Realtime Database
+-- Multi-region availability --> Firestore
+-- Very frequent small updates (typing indicators) --> Realtime Database
+-- For most new projects --> Firestore (recommended default)
Auth Method Selection
What auth flow does the user need?
+-- Email + Password --> createUserWithEmailAndPassword / signInWithEmailAndPassword
+-- Social login (Google, GitHub, etc.) --> signInWithPopup / signInWithRedirect
+-- Phone + SMS --> signInWithPhoneNumber (requires reCAPTCHA)
+-- Email link (passwordless) --> sendSignInLinkToEmail
+-- Custom backend auth --> Admin SDK createCustomToken + client signInWithCustomToken
+-- Anonymous (guest) --> signInAnonymously (upgrade later with linkWithCredential)
Cloud Functions: onRequest vs onCall
Who is calling the function?
+-- External webhooks, third-party services --> onRequest (raw HTTP)
+-- Your own client app (web/mobile) --> onCall (automatic auth, input validation)
+-- Firestore document changes --> onDocumentCreated / onDocumentUpdated / onDocumentDeleted
+-- Scheduled/cron tasks --> onSchedule
+-- Authentication events --> onUserCreated / onUserDeleted (from firebase-functions/v2/identity)
Client SDK vs Admin SDK
Where is the code running?
+-- Browser / Client-side --> Client SDK (firebase) -- security rules enforced
+-- Cloud Functions --> Admin SDK (firebase-admin) -- bypasses security rules
+-- API server / backend --> Admin SDK (firebase-admin) -- use service account
+-- NEVER use Admin SDK in client code --> It bypasses all security
Storage: When to Use Firebase Storage
What kind of files?
+-- User-generated content (avatars, uploads) --> Firebase Storage with security rules
+-- Public static assets (CSS, images) --> Firebase Hosting (faster CDN)
+-- Large files with progress tracking --> Firebase Storage with uploadBytesResumable
+-- Server-generated files (reports, exports) --> Firebase Storage via Admin SDK
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Wide-open security rules in production -- The default test-mode rules (
allow read, write: if true) give everyone full access to your entire database. This is the single most common Firebase security vulnerability. - Admin SDK credentials in client code -- Service account keys or Admin SDK imports in browser bundles give attackers full bypass of all security rules.
- Using the compat/namespace API --
firebase/compat/*imports prevent tree-shaking, bundling the entire SDK. The compat layer will be removed in a future major version. - Not handling auth state changes -- Calling Firestore without waiting for
onAuthStateChangedcan result in unauthenticated requests that fail silently against security rules.
Medium Priority Issues:
- Using v1 Cloud Functions API -- v1 (
functions.https.onRequest) lacks concurrency, traffic splitting, and longer timeouts. All new functions should use v2 (firebase-functions/v2/https). - Not unsubscribing from
onSnapshot-- Leaked listeners keep WebSocket connections open, consume bandwidth, and cause memory leaks. - Unbounded queries -- Queries without
limit()can fetch thousands of documents, causing performance issues and high read costs. - Monotonically increasing document IDs -- Sequential IDs like
user1,user2create hotspots. Use Firestore auto-generated IDs or UUIDs. - Using
functions.config()-- Deprecated and will fail after March 2027. Use Cloud Secret Manager (defineSecret()) or environment variables.
Common Mistakes:
- Not adding
.select()after Admin SDK queries -- Withoutselect(), all document fields are returned, increasing data transfer. - Calling
getFirestore()on every operation -- Initialize once and reuse the instance. Each call creates overhead. - Missing composite indexes -- Compound queries fail at runtime if the required composite index doesn't exist. Check error messages for the auto-generated index creation link.
- Deploying without testing security rules -- Use the emulator to test rules before deploying. Use
firebase emulators:exec "npm test"in CI. - Using
enableIndexedDbPersistence-- Deprecated. UseinitializeFirestorewithpersistentLocalCacheinstead.
Gotchas & Edge Cases:
- Firestore queries are "all or nothing" with security rules -- If a query could potentially return documents the user isn't allowed to read, the entire query fails (not just the unauthorized documents).
- Firestore limits -- 1 MB max document size, 20,000 fields per document, 500 writes per batch, 1 write per second per document sustained.
- Security rules propagation delay -- Rule updates take up to 1 minute to affect new queries, and up to 10 minutes for active listeners.
serverTimestamp()returnsnullinonSnapshotpending writes -- Until the server confirms the write, the timestamp field isnulllocally. Handle this with{ serverTimestamps: 'estimate' }in snapshot options.- Firebase Hosting has a 60-second timeout -- Even if your Cloud Function has a longer timeout, requests through Hosting rewrites timeout at 60 seconds.
onAuthStateChangedfires on page load -- It fires withnullinitially, then with the user if a session exists. Always handle the initialnullstate.- Firestore
inqueries are limited to 30 values --where("field", "in", array)supports a maximum of 30 elements in the array (increased from 10 in recent versions). - Cloud Functions cold starts -- First invocation after idle has additional latency. Use
onInit()for lazy initialization and keep dependencies minimal. - Admin SDK
initializeApp()should be called once -- Multiple calls throw an error unless you provide a unique app name. Guard with a try-catch or checkgetApps().length.
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use the modular Firebase SDK imports (firebase/app, firebase/firestore, firebase/auth) -- NEVER use the deprecated firebase/compat namespace API)
(You MUST write Firestore security rules for EVERY collection -- a collection without rules is wide open in production)
(You MUST NEVER expose Firebase Admin SDK credentials or service account keys in client-side code)
(You MUST use Cloud Functions v2 API (firebase-functions/v2/https, firebase-functions/v2/firestore) -- NOT the deprecated v1 API)
(You MUST handle all Firestore operations with error checking -- never assume reads/writes succeed)
Failure to follow these rules will create security vulnerabilities, bloated bundles, deprecated code paths, and silent runtime failures.
</critical_reminders>
Files (skills)
-
examples
-
auth.md 5.3 KB
# Firebase -- Authentication Examples > Email/password auth, OAuth providers, auth state management, and token handling. See [SKILL.md](../SKILL.md) for core concepts and [reference.md](../reference.md) for quick lookups. **Related examples:** - [core.md](core.md) -- Firebase project setup, emulator suite - [firestore.md](firestore.md) -- Firestore CRUD, queries, real-time listeners - [functions.md](functions.md) -- Cloud Functions v2, Admin SDK - [storage.md](storage.md) -- File upload, download, delete - [security-rules.md](security-rules.md) -- Firestore and Storage security rules --- ## Email/Password Auth ```typescript import { createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut, onAuthStateChanged, type User, type Unsubscribe, } from "firebase/auth"; import { auth } from "../lib/firebase"; // Sign up async function signUp(email: string, password: string): Promise<User> { const credential = await createUserWithEmailAndPassword( auth, email, password, ); return credential.user; } // Sign in async function signIn(email: string, password: string): Promise<User> { const credential = await signInWithEmailAndPassword(auth, email, password); return credential.user; } // Sign out async function logOut(): Promise<void> { await signOut(auth); } // Listen to auth state -- register early in app lifecycle function subscribeToAuthState( callback: (user: User | null) => void, ): Unsubscribe { return onAuthStateChanged(auth, callback); } ``` --- ## OAuth (Social Login) ```typescript import { signInWithPopup, signInWithRedirect, GoogleAuthProvider, GithubAuthProvider, type User, } from "firebase/auth"; import { auth } from "../lib/firebase"; const googleProvider = new GoogleAuthProvider(); const githubProvider = new GithubAuthProvider(); // Sign in with Google (popup) async function signInWithGoogle(): Promise<User> { const result = await signInWithPopup(auth, googleProvider); return result.user; } // Sign in with GitHub (redirect -- better for mobile) async function signInWithGithub(): Promise<void> { await signInWithRedirect(auth, githubProvider); } ``` --- ## Get Current User Token (for API calls) ```typescript import { auth } from "../lib/firebase"; async function getIdToken(): Promise<string | null> { const user = auth.currentUser; if (!user) { return null; } // Force refresh if token is expired return user.getIdToken(/* forceRefresh */ true); } // Use in API calls async function fetchFromApi(endpoint: string): Promise<Response> { const token = await getIdToken(); if (!token) { throw new Error("User not authenticated"); } return fetch(endpoint, { headers: { Authorization: `Bearer ${token}` }, }); } ``` **Key patterns:** Modular imports for each auth function, typed `User` return values, `Unsubscribe` for cleanup, separate providers as constants, token retrieval with force refresh for API calls --- ## Auth Service with User Profile Sync A complete auth service that manages sign-up with Firestore profile creation, OAuth with profile sync, and auth state observation. ```typescript // services/auth-service.ts import { onAuthStateChanged, signInWithEmailAndPassword, createUserWithEmailAndPassword, signInWithPopup, GoogleAuthProvider, signOut, type User, type Unsubscribe, } from "firebase/auth"; import { doc, setDoc, serverTimestamp } from "firebase/firestore"; import { auth, db } from "../lib/firebase"; // --- Constants --- const USERS_COLLECTION = "users"; // --- Auth State Observer --- export function subscribeToAuthState( callback: (user: User | null) => void, ): Unsubscribe { return onAuthStateChanged(auth, callback); } // --- Sign Up with Profile Creation --- const googleProvider = new GoogleAuthProvider(); export async function signUp( email: string, password: string, displayName: string, ): Promise<User> { const credential = await createUserWithEmailAndPassword( auth, email, password, ); // Create user profile document in Firestore await setDoc(doc(db, USERS_COLLECTION, credential.user.uid), { email, displayName, createdAt: serverTimestamp(), }); return credential.user; } // --- OAuth with Profile Sync --- export async function signInWithGoogle(): Promise<User> { const result = await signInWithPopup(auth, googleProvider); // Create/update user profile on first OAuth sign-in await setDoc( doc(db, USERS_COLLECTION, result.user.uid), { email: result.user.email, displayName: result.user.displayName, photoURL: result.user.photoURL, lastLoginAt: serverTimestamp(), }, { merge: true }, ); return result.user; } // --- Sign In / Sign Out --- export async function signIn(email: string, password: string): Promise<User> { const credential = await signInWithEmailAndPassword(auth, email, password); return credential.user; } export async function logOut(): Promise<void> { await signOut(auth); } ``` **Key patterns:** Framework-agnostic auth service (wire into your framework's state/context layer), `subscribeToAuthState` returns `Unsubscribe` for cleanup, profile creation on sign-up with `serverTimestamp()`, `merge: true` for OAuth profile updates (don't overwrite existing data), typed `User` return values --- _For core concepts, see [SKILL.md](../SKILL.md). For quick reference tables, see [reference.md](../reference.md)._ -
core.md 5.4 KB
# Firebase -- Core Examples > Project initialization, modular SDK setup, emulator connections, and environment configuration. See [SKILL.md](../SKILL.md) for core concepts and [reference.md](../reference.md) for quick lookups. **Related examples:** - [firestore.md](firestore.md) -- Firestore CRUD, queries, real-time listeners - [auth.md](auth.md) -- Authentication flows - [functions.md](functions.md) -- Cloud Functions v2, Admin SDK - [storage.md](storage.md) -- File upload, download, delete - [security-rules.md](security-rules.md) -- Firestore and Storage security rules --- ## Firebase App Initialization (Modular SDK) Initialize Firebase with the modular SDK for tree-shaking. Each service has its own import path. ```typescript // lib/firebase.ts import { initializeApp } from "firebase/app"; import { getFirestore } from "firebase/firestore"; import { getAuth } from "firebase/auth"; import { getStorage } from "firebase/storage"; import { getFunctions } from "firebase/functions"; const firebaseConfig = { apiKey: process.env.FIREBASE_API_KEY!, authDomain: process.env.FIREBASE_AUTH_DOMAIN!, projectId: process.env.FIREBASE_PROJECT_ID!, storageBucket: process.env.FIREBASE_STORAGE_BUCKET!, messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID!, appId: process.env.FIREBASE_APP_ID!, }; // Initialize Firebase -- call once at app startup const app = initializeApp(firebaseConfig); // Export service instances export const db = getFirestore(app); export const auth = getAuth(app); export const storage = getStorage(app); export const functions = getFunctions(app); ``` **Why good:** Modular imports enable tree-shaking, environment variables keep config out of code, each service initialized from the app instance, named exports for all services ```typescript // BAD: Legacy compat namespace API import firebase from "firebase/compat/app"; import "firebase/compat/firestore"; import "firebase/compat/auth"; firebase.initializeApp({ apiKey: "AIza...", // Hardcoded projectId: "my-project", }); const db = firebase.firestore(); const auth = firebase.auth(); export default { db, auth }; ``` **Why bad:** Compat imports prevent tree-shaking (entire SDK bundled), hardcoded credentials leak in source control, side-effect imports, default export --- ## Emulator Suite for Local Development Use the Firebase Emulator Suite for local development and testing without touching production data. ### Connect to Emulators ```typescript // lib/firebase.ts -- add emulator connections in development import { initializeApp } from "firebase/app"; import { getFirestore, connectFirestoreEmulator } from "firebase/firestore"; import { getAuth, connectAuthEmulator } from "firebase/auth"; import { getStorage, connectStorageEmulator } from "firebase/storage"; import { getFunctions, connectFunctionsEmulator } from "firebase/functions"; const app = initializeApp(firebaseConfig); export const db = getFirestore(app); export const auth = getAuth(app); export const storage = getStorage(app); export const functions = getFunctions(app); const FIRESTORE_EMULATOR_PORT = 8080; const AUTH_EMULATOR_PORT = 9099; const STORAGE_EMULATOR_PORT = 9199; const FUNCTIONS_EMULATOR_PORT = 5001; if (process.env.NODE_ENV === "development") { connectFirestoreEmulator(db, "localhost", FIRESTORE_EMULATOR_PORT); connectAuthEmulator(auth, `http://localhost:${AUTH_EMULATOR_PORT}`); connectStorageEmulator(storage, "localhost", STORAGE_EMULATOR_PORT); connectFunctionsEmulator(functions, "localhost", FUNCTIONS_EMULATOR_PORT); } ``` ### Start Emulators ```bash # Install Firebase CLI npm install -g firebase-tools # Initialize project (creates firebase.json) firebase init # Start all emulators with data persistence firebase emulators:start --import=./emulator-data --export-on-exit=./emulator-data # Start specific emulators only firebase emulators:start --only firestore,auth,functions # Run emulators and execute tests, then shut down firebase emulators:exec "npm test" ``` **Why good:** Emulator connections guarded by `NODE_ENV`, named constants for ports, `--import/--export-on-exit` persists emulator data between sessions, `emulators:exec` for CI pipelines --- ## Offline Persistence Enable Firestore offline persistence for apps that need to work without connectivity. ```typescript import { initializeFirestore, persistentLocalCache, persistentMultipleTabManager, } from "firebase/firestore"; import { initializeApp } from "firebase/app"; const app = initializeApp(firebaseConfig); // Enable persistent offline cache with multi-tab support export const db = initializeFirestore(app, { localCache: persistentLocalCache({ tabManager: persistentMultipleTabManager(), }), }); ``` **Why good:** `initializeFirestore` with `persistentLocalCache` is the modern approach (replaces deprecated `enableIndexedDbPersistence`), `persistentMultipleTabManager` enables multi-tab sync, configured at initialization time ```typescript // BAD: Deprecated persistence API import { getFirestore, enableIndexedDbPersistence } from "firebase/firestore"; const db = getFirestore(app); enableIndexedDbPersistence(db); // Deprecated -- use initializeFirestore instead ``` **Why bad:** `enableIndexedDbPersistence` is deprecated, must be called before any other Firestore operations, does not support multi-tab by default --- _For core concepts, see [SKILL.md](../SKILL.md). For quick reference tables, see [reference.md](../reference.md)._ -
firebase.md 23.9 KB
# Firebase Examples > Practical examples for Firestore CRUD, authentication flows, Cloud Functions v2, and Storage. See [SKILL.md](../SKILL.md) for core concepts and [reference.md](../reference.md) for quick lookups. --- ## Example 1: Complete Firestore CRUD Service A full-featured service for managing posts with typed documents, error handling, and pagination. ```typescript // services/post-service.ts import { collection, doc, getDoc, getDocs, setDoc, updateDoc, deleteDoc, query, where, orderBy, limit, startAfter, serverTimestamp, type DocumentSnapshot, type QueryDocumentSnapshot, Timestamp, } from "firebase/firestore"; import { db } from "../lib/firebase"; // --- Types --- export interface Post { id: string; title: string; content: string; authorId: string; published: boolean; tags: string[]; createdAt: Timestamp; updatedAt: Timestamp; } export type PostCreate = Omit<Post, "id" | "createdAt" | "updatedAt">; export type PostUpdate = Partial<Omit<Post, "id" | "createdAt" | "updatedAt">>; export interface PaginatedResult<T> { items: T[]; lastDoc: QueryDocumentSnapshot | null; hasMore: boolean; } // --- Constants --- const POSTS_COLLECTION = "posts"; const DEFAULT_PAGE_SIZE = 20; // --- Helpers --- function mapDoc(doc: DocumentSnapshot): Post { if (!doc.exists()) { throw new Error(`Document not found: ${doc.id}`); } return { id: doc.id, ...doc.data() } as Post; } function mapDocs(docs: QueryDocumentSnapshot[]): Post[] { return docs.map((doc) => ({ id: doc.id, ...doc.data() }) as Post); } // --- CRUD Operations --- export async function getPost(postId: string): Promise<Post> { const docRef = doc(db, POSTS_COLLECTION, postId); const snapshot = await getDoc(docRef); return mapDoc(snapshot); } export async function createPost(data: PostCreate): Promise<string> { const docRef = doc(collection(db, POSTS_COLLECTION)); await setDoc(docRef, { ...data, createdAt: serverTimestamp(), updatedAt: serverTimestamp(), }); return docRef.id; } export async function updatePost( postId: string, data: PostUpdate, ): Promise<void> { const docRef = doc(db, POSTS_COLLECTION, postId); await updateDoc(docRef, { ...data, updatedAt: serverTimestamp(), }); } export async function deletePost(postId: string): Promise<void> { const docRef = doc(db, POSTS_COLLECTION, postId); await deleteDoc(docRef); } // --- Queries --- export async function getPublishedPosts( pageSize: number = DEFAULT_PAGE_SIZE, lastDoc?: QueryDocumentSnapshot, ): Promise<PaginatedResult<Post>> { const postsRef = collection(db, POSTS_COLLECTION); const constraints = [ where("published", "==", true), orderBy("createdAt", "desc"), limit(pageSize + 1), // Fetch one extra to check if there's more ]; if (lastDoc) { constraints.push(startAfter(lastDoc)); } const q = query(postsRef, ...constraints); const snapshot = await getDocs(q); const hasMore = snapshot.docs.length > pageSize; const docs = hasMore ? snapshot.docs.slice(0, pageSize) : snapshot.docs; return { items: mapDocs(docs), lastDoc: docs.length > 0 ? docs[docs.length - 1] : null, hasMore, }; } export async function getPostsByAuthor(authorId: string): Promise<Post[]> { const postsRef = collection(db, POSTS_COLLECTION); const q = query( postsRef, where("authorId", "==", authorId), orderBy("createdAt", "desc"), ); const snapshot = await getDocs(q); return mapDocs(snapshot.docs); } export async function getPostsByTag(tag: string): Promise<Post[]> { const postsRef = collection(db, POSTS_COLLECTION); const q = query( postsRef, where("tags", "array-contains", tag), orderBy("createdAt", "desc"), limit(DEFAULT_PAGE_SIZE), ); const snapshot = await getDocs(q); return mapDocs(snapshot.docs); } ``` **Key patterns:** Typed document interfaces with `Omit<>` for create/update variants, `serverTimestamp()` for consistent timestamps, cursor-based pagination with `startAfter()`, "fetch N+1" pattern to detect `hasMore`, helper functions for doc mapping --- ## Example 2: Real-Time Chat with Firestore Listeners A complete chat implementation using `onSnapshot` for real-time updates. ```typescript // services/chat-service.ts import { collection, doc, setDoc, query, where, orderBy, limit, onSnapshot, serverTimestamp, type Unsubscribe, type Timestamp, } from "firebase/firestore"; import { db, auth } from "../lib/firebase"; // --- Types --- export interface Message { id: string; text: string; senderId: string; senderName: string; roomId: string; createdAt: Timestamp; } export interface ChatRoom { id: string; name: string; memberIds: string[]; lastMessage?: string; lastMessageAt?: Timestamp; } // --- Constants --- const ROOMS_COLLECTION = "rooms"; const MESSAGES_SUBCOLLECTION = "messages"; const RECENT_MESSAGES_LIMIT = 50; // --- Real-Time Listeners --- export function subscribeToMessages( roomId: string, onUpdate: (messages: Message[]) => void, onError?: (error: Error) => void, ): Unsubscribe { const messagesRef = collection( db, ROOMS_COLLECTION, roomId, MESSAGES_SUBCOLLECTION, ); const q = query( messagesRef, orderBy("createdAt", "asc"), limit(RECENT_MESSAGES_LIMIT), ); return onSnapshot( q, (snapshot) => { const messages = snapshot.docs.map( (doc) => ({ id: doc.id, ...doc.data() }) as Message, ); onUpdate(messages); }, (error) => { if (onError) { onError(new Error(`Chat listener failed: ${error.message}`)); } }, ); } export function subscribeToUserRooms( userId: string, onUpdate: (rooms: ChatRoom[]) => void, ): Unsubscribe { const roomsRef = collection(db, ROOMS_COLLECTION); const q = query( roomsRef, where("memberIds", "array-contains", userId), orderBy("lastMessageAt", "desc"), ); return onSnapshot(q, (snapshot) => { const rooms = snapshot.docs.map( (doc) => ({ id: doc.id, ...doc.data() }) as ChatRoom, ); onUpdate(rooms); }); } // --- Send Message --- export async function sendMessage(roomId: string, text: string): Promise<void> { const user = auth.currentUser; if (!user) { throw new Error("Must be signed in to send messages"); } const messagesRef = collection( db, ROOMS_COLLECTION, roomId, MESSAGES_SUBCOLLECTION, ); const messageRef = doc(messagesRef); // Write message and update room's last message in parallel const roomRef = doc(db, ROOMS_COLLECTION, roomId); await Promise.all([ setDoc(messageRef, { text, senderId: user.uid, senderName: user.displayName ?? "Anonymous", roomId, createdAt: serverTimestamp(), }), setDoc( roomRef, { lastMessage: text, lastMessageAt: serverTimestamp(), }, { merge: true }, ), ]); } ``` **Key patterns:** Subcollection pattern for messages within rooms, `array-contains` for membership queries, `onSnapshot` with error callback, `Unsubscribe` return type, `merge: true` for partial document updates, auth check before writes --- ## Example 3: Authentication with React Context A complete auth context pattern using Firebase Authentication with React. ```typescript // contexts/auth-context.tsx import { createContext, useContext, useEffect, useState, type ReactNode, } from "react"; import { onAuthStateChanged, signInWithEmailAndPassword, createUserWithEmailAndPassword, signInWithPopup, GoogleAuthProvider, signOut, type User, } from "firebase/auth"; import { doc, setDoc, serverTimestamp } from "firebase/firestore"; import { auth, db } from "../lib/firebase"; // --- Types --- interface AuthContextValue { user: User | null; loading: boolean; signIn: (email: string, password: string) => Promise<void>; signUp: (email: string, password: string, displayName: string) => Promise<void>; signInWithGoogle: () => Promise<void>; logOut: () => Promise<void>; } // --- Constants --- const USERS_COLLECTION = "users"; // --- Context --- const AuthContext = createContext<AuthContextValue | null>(null); export function useAuth(): AuthContextValue { const context = useContext(AuthContext); if (!context) { throw new Error("useAuth must be used within an AuthProvider"); } return context; } // --- Provider --- const googleProvider = new GoogleAuthProvider(); export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => { setUser(firebaseUser); setLoading(false); }); return unsubscribe; }, []); async function signIn(email: string, password: string): Promise<void> { await signInWithEmailAndPassword(auth, email, password); } async function signUp( email: string, password: string, displayName: string ): Promise<void> { const credential = await createUserWithEmailAndPassword( auth, email, password ); // Create user profile document in Firestore await setDoc(doc(db, USERS_COLLECTION, credential.user.uid), { email, displayName, createdAt: serverTimestamp(), }); } async function signInWithGoogle(): Promise<void> { const result = await signInWithPopup(auth, googleProvider); // Create/update user profile on first OAuth sign-in await setDoc( doc(db, USERS_COLLECTION, result.user.uid), { email: result.user.email, displayName: result.user.displayName, photoURL: result.user.photoURL, lastLoginAt: serverTimestamp(), }, { merge: true } ); } async function logOut(): Promise<void> { await signOut(auth); } const value: AuthContextValue = { user, loading, signIn, signUp, signInWithGoogle, logOut, }; return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; } // --- Usage --- // function ProtectedPage() { // const { user, loading, logOut } = useAuth(); // // if (loading) return <div>Loading...</div>; // if (!user) return <Navigate to="/login" />; // // return ( // <div> // <p>Welcome, {user.displayName}</p> // <button onClick={logOut}>Sign Out</button> // </div> // ); // } ``` **Key patterns:** Auth state in React context, `loading` state for initial auth check, `onAuthStateChanged` cleanup in `useEffect`, user profile creation on sign-up, `merge: true` for OAuth profile updates (don't overwrite existing data), typed context value --- ## Example 4: Cloud Functions v2 — Complete API A full API example with HTTP endpoints, callable functions, and Firestore triggers. ```typescript // functions/src/index.ts import { initializeApp } from "firebase-admin/app"; import { getFirestore, FieldValue } from "firebase-admin/firestore"; import { getAuth } from "firebase-admin/auth"; // Initialize Admin SDK (call once, before any function definitions) initializeApp(); const db = getFirestore(); const adminAuth = getAuth(); // Re-export functions from separate modules export { getPosts, getPostById } from "./api/posts"; export { createPost, updatePost } from "./api/post-mutations"; export { onPostCreated, onPostDeleted } from "./triggers/post-triggers"; export { cleanupExpiredSessions } from "./scheduled/cleanup"; ``` ```typescript // functions/src/api/posts.ts import { onRequest } from "firebase-functions/v2/https"; import { getFirestore } from "firebase-admin/firestore"; const db = getFirestore(); const POSTS_COLLECTION = "posts"; const DEFAULT_LIMIT = 20; export const getPosts = onRequest( { cors: true, region: "us-central1", memory: "256MiB" }, async (req, res) => { if (req.method !== "GET") { res.status(405).json({ error: "Method not allowed" }); return; } try { const pageSize = Math.min( Number(req.query.limit) || DEFAULT_LIMIT, DEFAULT_LIMIT, ); const snapshot = await db .collection(POSTS_COLLECTION) .where("published", "==", true) .orderBy("createdAt", "desc") .limit(pageSize) .get(); const posts = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data(), })); res.json({ posts, count: posts.length }); } catch (error) { console.error("Failed to fetch posts:", error); res.status(500).json({ error: "Internal server error" }); } }, ); export const getPostById = onRequest( { cors: true, region: "us-central1" }, async (req, res) => { const postId = req.path.split("/").pop(); if (!postId) { res.status(400).json({ error: "Post ID required" }); return; } try { const doc = await db.collection(POSTS_COLLECTION).doc(postId).get(); if (!doc.exists) { res.status(404).json({ error: "Post not found" }); return; } res.json({ id: doc.id, ...doc.data() }); } catch (error) { console.error("Failed to fetch post:", error); res.status(500).json({ error: "Internal server error" }); } }, ); ``` ```typescript // functions/src/api/post-mutations.ts import { onCall, HttpsError } from "firebase-functions/v2/https"; import { getFirestore, FieldValue } from "firebase-admin/firestore"; const db = getFirestore(); const POSTS_COLLECTION = "posts"; const MAX_TITLE_LENGTH = 200; const MAX_CONTENT_LENGTH = 50000; export const createPost = onCall({ region: "us-central1" }, async (request) => { if (!request.auth) { throw new HttpsError("unauthenticated", "Must be signed in"); } const { title, content, tags } = request.data; // Input validation if (!title || typeof title !== "string" || title.length > MAX_TITLE_LENGTH) { throw new HttpsError( "invalid-argument", `Title is required and must be under ${MAX_TITLE_LENGTH} characters`, ); } if ( !content || typeof content !== "string" || content.length > MAX_CONTENT_LENGTH ) { throw new HttpsError( "invalid-argument", `Content is required and must be under ${MAX_CONTENT_LENGTH} characters`, ); } const docRef = await db.collection(POSTS_COLLECTION).add({ title: title.trim(), content: content.trim(), tags: Array.isArray(tags) ? tags : [], authorId: request.auth.uid, published: false, createdAt: FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(), }); return { id: docRef.id }; }); export const updatePost = onCall({ region: "us-central1" }, async (request) => { if (!request.auth) { throw new HttpsError("unauthenticated", "Must be signed in"); } const { postId, ...updates } = request.data; if (!postId) { throw new HttpsError("invalid-argument", "Post ID is required"); } // Verify ownership const postSnap = await db.collection(POSTS_COLLECTION).doc(postId).get(); if (!postSnap.exists) { throw new HttpsError("not-found", "Post not found"); } if (postSnap.data()?.authorId !== request.auth.uid) { throw new HttpsError( "permission-denied", "You can only edit your own posts", ); } await db .collection(POSTS_COLLECTION) .doc(postId) .update({ ...updates, updatedAt: FieldValue.serverTimestamp(), }); return { success: true }; }); ``` ```typescript // functions/src/triggers/post-triggers.ts import { onDocumentCreated, onDocumentDeleted, } from "firebase-functions/v2/firestore"; import { getFirestore, FieldValue } from "firebase-admin/firestore"; const db = getFirestore(); const USERS_COLLECTION = "users"; export const onPostCreated = onDocumentCreated( { document: "posts/{postId}", region: "us-central1" }, async (event) => { const data = event.data?.data(); if (!data) { return; } // Increment the author's post count await db .collection(USERS_COLLECTION) .doc(data.authorId) .update({ postCount: FieldValue.increment(1), lastPostAt: FieldValue.serverTimestamp(), }); }, ); export const onPostDeleted = onDocumentDeleted( { document: "posts/{postId}", region: "us-central1" }, async (event) => { const data = event.data?.data(); if (!data) { return; } // Decrement the author's post count await db .collection(USERS_COLLECTION) .doc(data.authorId) .update({ postCount: FieldValue.increment(-1), }); }, ); ``` ```typescript // functions/src/scheduled/cleanup.ts import { onSchedule } from "firebase-functions/v2/scheduler"; import { getFirestore } from "firebase-admin/firestore"; const db = getFirestore(); const SESSIONS_COLLECTION = "sessions"; const SESSION_EXPIRY_DAYS = 30; const MAX_BATCH_SIZE = 500; export const cleanupExpiredSessions = onSchedule( { schedule: "every day 03:00", region: "us-central1", timeZone: "America/New_York", }, async () => { const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - SESSION_EXPIRY_DAYS); const expired = await db .collection(SESSIONS_COLLECTION) .where("lastActive", "<", cutoff) .limit(MAX_BATCH_SIZE) .get(); if (expired.empty) { console.log("No expired sessions to clean up"); return; } const batch = db.batch(); for (const doc of expired.docs) { batch.delete(doc.ref); } await batch.commit(); console.log(`Deleted ${expired.docs.length} expired sessions`); }, ); ``` **Key patterns:** Modular v2 imports from subpackages, `HttpsError` for typed error codes in callable functions, auth verification in callable functions, input validation with length limits, Firestore triggers for denormalized counter updates, scheduled functions with timezone, Admin SDK for server-side operations, `FieldValue.increment()` for atomic counters, named constants throughout --- ## Example 5: Storage Upload with Progress and Validation Complete file upload flow with client-side validation, progress tracking, and security rules. ```typescript // services/storage-service.ts import { ref, uploadBytesResumable, getDownloadURL, deleteObject, listAll, type UploadTaskSnapshot, } from "firebase/storage"; import { storage, auth } from "../lib/firebase"; // --- Constants --- const MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB const MAX_DOCUMENT_SIZE_BYTES = 20 * 1024 * 1024; // 20 MB const ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp"]; const ALLOWED_DOCUMENT_TYPES = [ "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ]; // --- Types --- export interface UploadResult { url: string; path: string; } export interface UploadProgress { bytesTransferred: number; totalBytes: number; percent: number; } // --- Validation --- function validateImageFile(file: File): void { if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { throw new Error( `Invalid image type: ${file.type}. Allowed: ${ALLOWED_IMAGE_TYPES.join(", ")}`, ); } if (file.size > MAX_IMAGE_SIZE_BYTES) { throw new Error("Image exceeds 5MB size limit"); } } function validateDocumentFile(file: File): void { if (!ALLOWED_DOCUMENT_TYPES.includes(file.type)) { throw new Error(`Invalid document type: ${file.type}`); } if (file.size > MAX_DOCUMENT_SIZE_BYTES) { throw new Error("Document exceeds 20MB size limit"); } } // --- Upload Functions --- export function uploadImage( file: File, path: string, onProgress?: (progress: UploadProgress) => void, ): Promise<UploadResult> { validateImageFile(file); return uploadFile(file, path, onProgress); } export function uploadDocument( file: File, path: string, onProgress?: (progress: UploadProgress) => void, ): Promise<UploadResult> { validateDocumentFile(file); return uploadFile(file, path, onProgress); } function uploadFile( file: File, path: string, onProgress?: (progress: UploadProgress) => void, ): Promise<UploadResult> { const user = auth.currentUser; if (!user) { throw new Error("Must be authenticated to upload files"); } const storageRef = ref(storage, path); const uploadTask = uploadBytesResumable(storageRef, file, { contentType: file.type, customMetadata: { uploadedBy: user.uid }, }); return new Promise((resolve, reject) => { const PERCENT_MULTIPLIER = 100; uploadTask.on( "state_changed", (snapshot: UploadTaskSnapshot) => { if (onProgress) { onProgress({ bytesTransferred: snapshot.bytesTransferred, totalBytes: snapshot.totalBytes, percent: (snapshot.bytesTransferred / snapshot.totalBytes) * PERCENT_MULTIPLIER, }); } }, (error) => reject(new Error(`Upload failed: ${error.message}`)), async () => { const url = await getDownloadURL(uploadTask.snapshot.ref); resolve({ url, path }); }, ); }); } // --- Delete --- export async function deleteFile(path: string): Promise<void> { const storageRef = ref(storage, path); await deleteObject(storageRef); } // --- List Files --- export async function listUserFiles(userId: string): Promise<string[]> { const listRef = ref(storage, `documents/${userId}`); const result = await listAll(listRef); return result.items.map((item) => item.fullPath); } ``` **Key patterns:** Client-side validation before upload (type and size), resumable upload with progress callback, auth check before upload, `customMetadata` for audit trail, typed upload result with URL and path, separate validation functions per file category --- ## Example 6: Calling Cloud Functions from Client Use `httpsCallable` to call `onCall` Cloud Functions from the client with automatic auth token handling. ```typescript // services/api-service.ts import { httpsCallable, type HttpsCallableResult } from "firebase/functions"; import { functions } from "../lib/firebase"; import type { Post, PostCreate, PostUpdate } from "../types/post"; // --- Types --- interface CreatePostResponse { id: string; } interface UpdatePostResponse { success: boolean; } // --- Callable Function Wrappers --- const createPostFn = httpsCallable<PostCreate, CreatePostResponse>( functions, "createPost", ); const updatePostFn = httpsCallable< PostUpdate & { postId: string }, UpdatePostResponse >(functions, "updatePost"); // --- Service Functions --- export async function createPost(data: PostCreate): Promise<string> { const result: HttpsCallableResult<CreatePostResponse> = await createPostFn(data); return result.data.id; } export async function updatePost( postId: string, data: PostUpdate, ): Promise<void> { await updatePostFn({ postId, ...data }); } ``` **Key patterns:** `httpsCallable` generic types for request and response, named function variables, wrapped in service functions for abstraction, auth token automatically included by the client SDK --- ## Example 7: App Check Setup Protect your backend resources from abuse by verifying requests come from your app. ```typescript // lib/firebase.ts — add App Check initialization import { initializeApp } from "firebase/app"; import { initializeAppCheck, ReCaptchaEnterpriseProvider, } from "firebase/app-check"; const app = initializeApp(firebaseConfig); // Enable App Check with reCAPTCHA Enterprise const RECAPTCHA_SITE_KEY = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!; // Enable debug token in development if (process.env.NODE_ENV === "development") { // @ts-expect-error -- Firebase debug token global self.FIREBASE_APPCHECK_DEBUG_TOKEN = true; } const appCheck = initializeAppCheck(app, { provider: new ReCaptchaEnterpriseProvider(RECAPTCHA_SITE_KEY), isTokenAutoRefreshEnabled: true, }); ``` **Key patterns:** reCAPTCHA Enterprise is the recommended provider (reCAPTCHA v3 is being phased out), debug token in development for emulator support, `isTokenAutoRefreshEnabled` keeps the token fresh, `@ts-expect-error` for the global debug token --- _For core concepts, see [SKILL.md](../SKILL.md). For quick reference tables, see [reference.md](../reference.md)._ -
firestore.md 9 KB
# Firebase -- Firestore Examples > CRUD operations, queries, real-time listeners, batch writes, transactions, and pagination. See [SKILL.md](../SKILL.md) for core concepts and [reference.md](../reference.md) for quick lookups. **Related examples:** - [core.md](core.md) -- Firebase project setup, emulator suite - [auth.md](auth.md) -- Authentication flows - [functions.md](functions.md) -- Cloud Functions v2, Admin SDK - [storage.md](storage.md) -- File upload, download, delete - [security-rules.md](security-rules.md) -- Firestore and Storage security rules --- ## Complete Firestore CRUD Service A full-featured service for managing posts with typed documents, error handling, and pagination. ```typescript // services/post-service.ts import { collection, doc, getDoc, getDocs, setDoc, updateDoc, deleteDoc, query, where, orderBy, limit, startAfter, serverTimestamp, type DocumentSnapshot, type QueryDocumentSnapshot, Timestamp, } from "firebase/firestore"; import { db } from "../lib/firebase"; // --- Types --- export interface Post { id: string; title: string; content: string; authorId: string; published: boolean; tags: string[]; createdAt: Timestamp; updatedAt: Timestamp; } export type PostCreate = Omit<Post, "id" | "createdAt" | "updatedAt">; export type PostUpdate = Partial<Omit<Post, "id" | "createdAt" | "updatedAt">>; export interface PaginatedResult<T> { items: T[]; lastDoc: QueryDocumentSnapshot | null; hasMore: boolean; } // --- Constants --- const POSTS_COLLECTION = "posts"; const DEFAULT_PAGE_SIZE = 20; // --- Helpers --- function mapDoc(doc: DocumentSnapshot): Post { if (!doc.exists()) { throw new Error(`Document not found: ${doc.id}`); } return { id: doc.id, ...doc.data() } as Post; } function mapDocs(docs: QueryDocumentSnapshot[]): Post[] { return docs.map((doc) => ({ id: doc.id, ...doc.data() }) as Post); } // --- CRUD Operations --- export async function getPost(postId: string): Promise<Post> { const docRef = doc(db, POSTS_COLLECTION, postId); const snapshot = await getDoc(docRef); return mapDoc(snapshot); } export async function createPost(data: PostCreate): Promise<string> { const docRef = doc(collection(db, POSTS_COLLECTION)); await setDoc(docRef, { ...data, createdAt: serverTimestamp(), updatedAt: serverTimestamp(), }); return docRef.id; } export async function updatePost( postId: string, data: PostUpdate, ): Promise<void> { const docRef = doc(db, POSTS_COLLECTION, postId); await updateDoc(docRef, { ...data, updatedAt: serverTimestamp(), }); } export async function deletePost(postId: string): Promise<void> { const docRef = doc(db, POSTS_COLLECTION, postId); await deleteDoc(docRef); } // --- Queries --- export async function getPublishedPosts( pageSize: number = DEFAULT_PAGE_SIZE, lastDoc?: QueryDocumentSnapshot, ): Promise<PaginatedResult<Post>> { const postsRef = collection(db, POSTS_COLLECTION); const constraints = [ where("published", "==", true), orderBy("createdAt", "desc"), limit(pageSize + 1), // Fetch one extra to check if there's more ]; if (lastDoc) { constraints.push(startAfter(lastDoc)); } const q = query(postsRef, ...constraints); const snapshot = await getDocs(q); const hasMore = snapshot.docs.length > pageSize; const docs = hasMore ? snapshot.docs.slice(0, pageSize) : snapshot.docs; return { items: mapDocs(docs), lastDoc: docs.length > 0 ? docs[docs.length - 1] : null, hasMore, }; } export async function getPostsByAuthor(authorId: string): Promise<Post[]> { const postsRef = collection(db, POSTS_COLLECTION); const q = query( postsRef, where("authorId", "==", authorId), orderBy("createdAt", "desc"), ); const snapshot = await getDocs(q); return mapDocs(snapshot.docs); } export async function getPostsByTag(tag: string): Promise<Post[]> { const postsRef = collection(db, POSTS_COLLECTION); const q = query( postsRef, where("tags", "array-contains", tag), orderBy("createdAt", "desc"), limit(DEFAULT_PAGE_SIZE), ); const snapshot = await getDocs(q); return mapDocs(snapshot.docs); } ``` **Key patterns:** Typed document interfaces with `Omit<>` for create/update variants, `serverTimestamp()` for consistent timestamps, cursor-based pagination with `startAfter()`, "fetch N+1" pattern to detect `hasMore`, helper functions for doc mapping --- ## Real-Time Chat with Firestore Listeners A complete chat implementation using `onSnapshot` for real-time updates. ```typescript // services/chat-service.ts import { collection, doc, setDoc, query, where, orderBy, limit, onSnapshot, serverTimestamp, type Unsubscribe, type Timestamp, } from "firebase/firestore"; import { db, auth } from "../lib/firebase"; // --- Types --- export interface Message { id: string; text: string; senderId: string; senderName: string; roomId: string; createdAt: Timestamp; } export interface ChatRoom { id: string; name: string; memberIds: string[]; lastMessage?: string; lastMessageAt?: Timestamp; } // --- Constants --- const ROOMS_COLLECTION = "rooms"; const MESSAGES_SUBCOLLECTION = "messages"; const RECENT_MESSAGES_LIMIT = 50; // --- Real-Time Listeners --- export function subscribeToMessages( roomId: string, onUpdate: (messages: Message[]) => void, onError?: (error: Error) => void, ): Unsubscribe { const messagesRef = collection( db, ROOMS_COLLECTION, roomId, MESSAGES_SUBCOLLECTION, ); const q = query( messagesRef, orderBy("createdAt", "asc"), limit(RECENT_MESSAGES_LIMIT), ); return onSnapshot( q, (snapshot) => { const messages = snapshot.docs.map( (doc) => ({ id: doc.id, ...doc.data() }) as Message, ); onUpdate(messages); }, (error) => { if (onError) { onError(new Error(`Chat listener failed: ${error.message}`)); } }, ); } export function subscribeToUserRooms( userId: string, onUpdate: (rooms: ChatRoom[]) => void, ): Unsubscribe { const roomsRef = collection(db, ROOMS_COLLECTION); const q = query( roomsRef, where("memberIds", "array-contains", userId), orderBy("lastMessageAt", "desc"), ); return onSnapshot(q, (snapshot) => { const rooms = snapshot.docs.map( (doc) => ({ id: doc.id, ...doc.data() }) as ChatRoom, ); onUpdate(rooms); }); } // --- Send Message --- export async function sendMessage(roomId: string, text: string): Promise<void> { const user = auth.currentUser; if (!user) { throw new Error("Must be signed in to send messages"); } const messagesRef = collection( db, ROOMS_COLLECTION, roomId, MESSAGES_SUBCOLLECTION, ); const messageRef = doc(messagesRef); // Write message and update room's last message in parallel const roomRef = doc(db, ROOMS_COLLECTION, roomId); await Promise.all([ setDoc(messageRef, { text, senderId: user.uid, senderName: user.displayName ?? "Anonymous", roomId, createdAt: serverTimestamp(), }), setDoc( roomRef, { lastMessage: text, lastMessageAt: serverTimestamp(), }, { merge: true }, ), ]); } ``` **Key patterns:** Subcollection pattern for messages within rooms, `array-contains` for membership queries, `onSnapshot` with error callback, `Unsubscribe` return type, `merge: true` for partial document updates, auth check before writes --- ## Transactions and Batched Writes Use transactions for atomic read-then-write operations. Use batched writes for multiple writes without reads. ```typescript import { runTransaction, writeBatch, doc, increment, serverTimestamp, } from "firebase/firestore"; import { db } from "../lib/firebase"; const POSTS_COLLECTION = "posts"; const USERS_COLLECTION = "users"; // Transaction: atomic read-then-write (e.g., increment a counter) async function likePost(postId: string, userId: string): Promise<void> { const postRef = doc(db, POSTS_COLLECTION, postId); const likeRef = doc(db, POSTS_COLLECTION, postId, "likes", userId); await runTransaction(db, async (transaction) => { const postSnap = await transaction.get(postRef); if (!postSnap.exists()) { throw new Error(`Post not found: ${postId}`); } transaction.update(postRef, { likeCount: increment(1) }); transaction.set(likeRef, { createdAt: serverTimestamp() }); }); } // Batched write: multiple writes without reads (up to 500 operations) const MAX_BATCH_SIZE = 500; async function deleteUserPosts(postIds: string[]): Promise<void> { const batch = writeBatch(db); for (const postId of postIds.slice(0, MAX_BATCH_SIZE)) { const docRef = doc(db, POSTS_COLLECTION, postId); batch.delete(docRef); } await batch.commit(); } ``` **Key patterns:** Transaction ensures atomic increment (no race conditions), `increment()` for safe counter updates, batch write for bulk operations, `MAX_BATCH_SIZE` constant documents the 500 limit --- _For core concepts, see [SKILL.md](../SKILL.md). For quick reference tables, see [reference.md](../reference.md)._ -
functions.md 10.9 KB
# Firebase -- Cloud Functions v2 & Admin SDK Examples > HTTP endpoints, callable functions, Firestore triggers, scheduled functions, and Admin SDK patterns. See [SKILL.md](../SKILL.md) for core concepts and [reference.md](../reference.md) for quick lookups. **Related examples:** - [core.md](core.md) -- Firebase project setup, emulator suite - [firestore.md](firestore.md) -- Firestore CRUD, queries, real-time listeners - [auth.md](auth.md) -- Authentication flows - [storage.md](storage.md) -- File upload, download, delete - [security-rules.md](security-rules.md) -- Firestore and Storage security rules --- ## Complete Cloud Functions v2 API A full API example with HTTP endpoints, callable functions, Firestore triggers, and scheduled tasks. ### Entry Point ```typescript // functions/src/index.ts import { initializeApp } from "firebase-admin/app"; import { getFirestore, FieldValue } from "firebase-admin/firestore"; import { getAuth } from "firebase-admin/auth"; // Initialize Admin SDK (call once, before any function definitions) initializeApp(); const db = getFirestore(); const adminAuth = getAuth(); // Re-export functions from separate modules export { getPosts, getPostById } from "./api/posts"; export { createPost, updatePost } from "./api/post-mutations"; export { onPostCreated, onPostDeleted } from "./triggers/post-triggers"; export { cleanupExpiredSessions } from "./scheduled/cleanup"; ``` ### HTTP Function (onRequest) ```typescript // functions/src/api/posts.ts import { onRequest } from "firebase-functions/v2/https"; import { getFirestore } from "firebase-admin/firestore"; const db = getFirestore(); const POSTS_COLLECTION = "posts"; const DEFAULT_LIMIT = 20; export const getPosts = onRequest( { cors: true, region: "us-central1", memory: "256MiB" }, async (req, res) => { if (req.method !== "GET") { res.status(405).json({ error: "Method not allowed" }); return; } try { const pageSize = Math.min( Number(req.query.limit) || DEFAULT_LIMIT, DEFAULT_LIMIT, ); const snapshot = await db .collection(POSTS_COLLECTION) .where("published", "==", true) .orderBy("createdAt", "desc") .limit(pageSize) .get(); const posts = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data(), })); res.json({ posts, count: posts.length }); } catch (error) { console.error("Failed to fetch posts:", error); res.status(500).json({ error: "Internal server error" }); } }, ); export const getPostById = onRequest( { cors: true, region: "us-central1" }, async (req, res) => { const postId = req.path.split("/").pop(); if (!postId) { res.status(400).json({ error: "Post ID required" }); return; } try { const doc = await db.collection(POSTS_COLLECTION).doc(postId).get(); if (!doc.exists) { res.status(404).json({ error: "Post not found" }); return; } res.json({ id: doc.id, ...doc.data() }); } catch (error) { console.error("Failed to fetch post:", error); res.status(500).json({ error: "Internal server error" }); } }, ); ``` ### Callable Function (onCall) ```typescript // functions/src/api/post-mutations.ts import { onCall, HttpsError } from "firebase-functions/v2/https"; import { getFirestore, FieldValue } from "firebase-admin/firestore"; const db = getFirestore(); const POSTS_COLLECTION = "posts"; const MAX_TITLE_LENGTH = 200; const MAX_CONTENT_LENGTH = 50000; export const createPost = onCall({ region: "us-central1" }, async (request) => { if (!request.auth) { throw new HttpsError("unauthenticated", "Must be signed in"); } const { title, content, tags } = request.data; // Input validation if (!title || typeof title !== "string" || title.length > MAX_TITLE_LENGTH) { throw new HttpsError( "invalid-argument", `Title is required and must be under ${MAX_TITLE_LENGTH} characters`, ); } if ( !content || typeof content !== "string" || content.length > MAX_CONTENT_LENGTH ) { throw new HttpsError( "invalid-argument", `Content is required and must be under ${MAX_CONTENT_LENGTH} characters`, ); } const docRef = await db.collection(POSTS_COLLECTION).add({ title: title.trim(), content: content.trim(), tags: Array.isArray(tags) ? tags : [], authorId: request.auth.uid, published: false, createdAt: FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(), }); return { id: docRef.id }; }); export const updatePost = onCall({ region: "us-central1" }, async (request) => { if (!request.auth) { throw new HttpsError("unauthenticated", "Must be signed in"); } const { postId, ...updates } = request.data; if (!postId) { throw new HttpsError("invalid-argument", "Post ID is required"); } // Verify ownership const postSnap = await db.collection(POSTS_COLLECTION).doc(postId).get(); if (!postSnap.exists) { throw new HttpsError("not-found", "Post not found"); } if (postSnap.data()?.authorId !== request.auth.uid) { throw new HttpsError( "permission-denied", "You can only edit your own posts", ); } await db .collection(POSTS_COLLECTION) .doc(postId) .update({ ...updates, updatedAt: FieldValue.serverTimestamp(), }); return { success: true }; }); ``` ### Firestore Trigger ```typescript // functions/src/triggers/post-triggers.ts import { onDocumentCreated, onDocumentDeleted, } from "firebase-functions/v2/firestore"; import { getFirestore, FieldValue } from "firebase-admin/firestore"; const db = getFirestore(); const USERS_COLLECTION = "users"; export const onPostCreated = onDocumentCreated( { document: "posts/{postId}", region: "us-central1" }, async (event) => { const data = event.data?.data(); if (!data) { return; } // Increment the author's post count await db .collection(USERS_COLLECTION) .doc(data.authorId) .update({ postCount: FieldValue.increment(1), lastPostAt: FieldValue.serverTimestamp(), }); }, ); export const onPostDeleted = onDocumentDeleted( { document: "posts/{postId}", region: "us-central1" }, async (event) => { const data = event.data?.data(); if (!data) { return; } // Decrement the author's post count await db .collection(USERS_COLLECTION) .doc(data.authorId) .update({ postCount: FieldValue.increment(-1), }); }, ); ``` ### Scheduled Function ```typescript // functions/src/scheduled/cleanup.ts import { onSchedule } from "firebase-functions/v2/scheduler"; import { getFirestore } from "firebase-admin/firestore"; const db = getFirestore(); const SESSIONS_COLLECTION = "sessions"; const SESSION_EXPIRY_DAYS = 30; const MAX_BATCH_SIZE = 500; export const cleanupExpiredSessions = onSchedule( { schedule: "every day 03:00", region: "us-central1", timeZone: "America/New_York", }, async () => { const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - SESSION_EXPIRY_DAYS); const expired = await db .collection(SESSIONS_COLLECTION) .where("lastActive", "<", cutoff) .limit(MAX_BATCH_SIZE) .get(); if (expired.empty) { console.log("No expired sessions to clean up"); return; } const batch = db.batch(); for (const doc of expired.docs) { batch.delete(doc.ref); } await batch.commit(); console.log(`Deleted ${expired.docs.length} expired sessions`); }, ); ``` **Key patterns:** Modular v2 imports from subpackages, `HttpsError` for typed error codes in callable functions, auth verification in callable functions, input validation with length limits, Firestore triggers for denormalized counter updates, scheduled functions with timezone, Admin SDK for server-side operations, `FieldValue.increment()` for atomic counters, named constants throughout --- ## Calling Cloud Functions from Client Use `httpsCallable` to call `onCall` Cloud Functions from the client with automatic auth token handling. ```typescript // services/api-service.ts import { httpsCallable, type HttpsCallableResult } from "firebase/functions"; import { functions } from "../lib/firebase"; import type { Post, PostCreate, PostUpdate } from "../types/post"; // --- Types --- interface CreatePostResponse { id: string; } interface UpdatePostResponse { success: boolean; } // --- Callable Function Wrappers --- const createPostFn = httpsCallable<PostCreate, CreatePostResponse>( functions, "createPost", ); const updatePostFn = httpsCallable< PostUpdate & { postId: string }, UpdatePostResponse >(functions, "updatePost"); // --- Service Functions --- export async function createPost(data: PostCreate): Promise<string> { const result: HttpsCallableResult<CreatePostResponse> = await createPostFn(data); return result.data.id; } export async function updatePost( postId: string, data: PostUpdate, ): Promise<void> { await updatePostFn({ postId, ...data }); } ``` **Key patterns:** `httpsCallable` generic types for request and response, named function variables, wrapped in service functions for abstraction, auth token automatically included by the client SDK --- ## Admin SDK (Server-Side) Use the Admin SDK in Cloud Functions, API routes, or any trusted server environment. The Admin SDK bypasses all security rules. ```typescript // functions/src/admin.ts import { initializeApp, cert } from "firebase-admin/app"; import { getFirestore } from "firebase-admin/firestore"; import { getAuth } from "firebase-admin/auth"; import { getStorage } from "firebase-admin/storage"; // In Cloud Functions, initializeApp() uses default credentials automatically initializeApp(); // In standalone servers, use a service account // initializeApp({ // credential: cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT!)), // }); export const adminDb = getFirestore(); export const adminAuth = getAuth(); export const adminStorage = getStorage(); // Create a custom token for a user async function createCustomToken( uid: string, claims?: object, ): Promise<string> { return adminAuth.createCustomToken(uid, claims); } // Verify an ID token from a client async function verifyIdToken(idToken: string) { return adminAuth.verifyIdToken(idToken); } // Admin Firestore operations bypass security rules async function adminGetUser(userId: string) { const userRecord = await adminAuth.getUser(userId); return userRecord; } // Set custom claims on a user (e.g., admin role) async function setAdminRole(uid: string): Promise<void> { await adminAuth.setCustomUserClaims(uid, { admin: true }); } ``` **Key patterns:** Modular Admin SDK imports (`firebase-admin/app`, `firebase-admin/firestore`), default credentials in Cloud Functions, explicit `cert()` for standalone servers, custom tokens for third-party auth integration, custom claims for role-based access --- _For core concepts, see [SKILL.md](../SKILL.md). For quick reference tables, see [reference.md](../reference.md)._ -
security-rules.md 4.4 KB
# Firebase -- Security Rules Examples > Firestore and Storage security rules: auth-based access, data validation, helper functions, and common patterns. See [SKILL.md](../SKILL.md) for core concepts and [reference.md](../reference.md) for quick lookups. **Related examples:** - [core.md](core.md) -- Firebase project setup, emulator suite - [firestore.md](firestore.md) -- Firestore CRUD, queries, real-time listeners - [auth.md](auth.md) -- Authentication flows - [functions.md](functions.md) -- Cloud Functions v2, Admin SDK - [storage.md](storage.md) -- File upload, download, delete --- ## Firestore Security Rules ``` // firestore.rules rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // Helper function: check if user is authenticated function isAuthenticated() { return request.auth != null; } // Helper function: check if user owns the resource function isOwner(userId) { return request.auth.uid == userId; } // Helper function: check if user has admin custom claim function isAdmin() { return request.auth.token.admin == true; } // Posts collection match /posts/{postId} { // Anyone can read published posts; authors can read their own drafts allow read: if resource.data.published == true || isOwner(resource.data.authorId); // Only authenticated users can create posts as themselves allow create: if isAuthenticated() && request.resource.data.authorId == request.auth.uid && request.resource.data.title is string && request.resource.data.title.size() > 0 && request.resource.data.title.size() <= 200; // Authors can update their own posts allow update: if isOwner(resource.data.authorId) && request.resource.data.authorId == resource.data.authorId; // Authors and admins can delete posts allow delete: if isOwner(resource.data.authorId) || isAdmin(); // Subcollection: likes match /likes/{likeId} { allow read: if true; allow create: if isAuthenticated() && likeId == request.auth.uid; allow delete: if isAuthenticated() && likeId == request.auth.uid; } } // User profiles match /users/{userId} { allow read: if true; allow create: if isOwner(userId); allow update: if isOwner(userId); allow delete: if false; // Users cannot delete their own profile document } } } ``` **Why good:** `rules_version = '2'` required for collection group queries, helper functions reduce duplication, granular read/write split into create/update/delete, data validation on create, ownership check prevents authorId spoofing on update, subcollection rules nested properly --- ## Storage Security Rules ``` // storage.rules rules_version = '2'; service firebase.storage { match /b/{bucket}/o { // Avatars: users can upload/read their own avatar match /avatars/{userId}/{fileName} { allow read: if true; allow write: if request.auth != null && request.auth.uid == userId && request.resource.size < 5 * 1024 * 1024 && request.resource.contentType.matches('image/.*'); } // Documents: authenticated users can upload, only owner can read match /documents/{userId}/{fileName} { allow read: if request.auth != null && request.auth.uid == userId; allow write: if request.auth != null && request.auth.uid == userId && request.resource.size < 10 * 1024 * 1024; } // Deny everything else by default match /{allPaths=**} { allow read, write: if false; } } } ``` **Why good:** File size limits enforced in rules, content type validation for images, owner-based access control, default deny for unmatched paths --- ## Bad Example: Wide-Open Rules ``` // BAD: Wide-open security rules rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if true; // DANGER: Anyone can read/write everything } } } ``` **Why bad:** No authentication check, no access control, no data validation, entire database is public read/write -- this is the default test-mode rule and must NEVER be deployed to production --- _For core concepts, see [SKILL.md](../SKILL.md). For quick reference tables, see [reference.md](../reference.md)._ -
setup.md 5.4 KB
# Firebase -- Core Examples > Project initialization, modular SDK setup, emulator connections, and environment configuration. See [SKILL.md](../SKILL.md) for core concepts and [reference.md](../reference.md) for quick lookups. **Related examples:** - [firestore.md](firestore.md) -- Firestore CRUD, queries, real-time listeners - [auth.md](auth.md) -- Authentication flows - [functions.md](functions.md) -- Cloud Functions v2, Admin SDK - [storage.md](storage.md) -- File upload, download, delete - [security-rules.md](security-rules.md) -- Firestore and Storage security rules --- ## Firebase App Initialization (Modular SDK) Initialize Firebase with the modular SDK for tree-shaking. Each service has its own import path. ```typescript // lib/firebase.ts import { initializeApp } from "firebase/app"; import { getFirestore } from "firebase/firestore"; import { getAuth } from "firebase/auth"; import { getStorage } from "firebase/storage"; import { getFunctions } from "firebase/functions"; const firebaseConfig = { apiKey: process.env.FIREBASE_API_KEY!, authDomain: process.env.FIREBASE_AUTH_DOMAIN!, projectId: process.env.FIREBASE_PROJECT_ID!, storageBucket: process.env.FIREBASE_STORAGE_BUCKET!, messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID!, appId: process.env.FIREBASE_APP_ID!, }; // Initialize Firebase -- call once at app startup const app = initializeApp(firebaseConfig); // Export service instances export const db = getFirestore(app); export const auth = getAuth(app); export const storage = getStorage(app); export const functions = getFunctions(app); ``` **Why good:** Modular imports enable tree-shaking, environment variables keep config out of code, each service initialized from the app instance, named exports for all services ```typescript // BAD: Legacy compat namespace API import firebase from "firebase/compat/app"; import "firebase/compat/firestore"; import "firebase/compat/auth"; firebase.initializeApp({ apiKey: "AIza...", // Hardcoded projectId: "my-project", }); const db = firebase.firestore(); const auth = firebase.auth(); export default { db, auth }; ``` **Why bad:** Compat imports prevent tree-shaking (entire SDK bundled), hardcoded credentials leak in source control, side-effect imports, default export --- ## Emulator Suite for Local Development Use the Firebase Emulator Suite for local development and testing without touching production data. ### Connect to Emulators ```typescript // lib/firebase.ts -- add emulator connections in development import { initializeApp } from "firebase/app"; import { getFirestore, connectFirestoreEmulator } from "firebase/firestore"; import { getAuth, connectAuthEmulator } from "firebase/auth"; import { getStorage, connectStorageEmulator } from "firebase/storage"; import { getFunctions, connectFunctionsEmulator } from "firebase/functions"; const app = initializeApp(firebaseConfig); export const db = getFirestore(app); export const auth = getAuth(app); export const storage = getStorage(app); export const functions = getFunctions(app); const FIRESTORE_EMULATOR_PORT = 8080; const AUTH_EMULATOR_PORT = 9099; const STORAGE_EMULATOR_PORT = 9199; const FUNCTIONS_EMULATOR_PORT = 5001; if (process.env.NODE_ENV === "development") { connectFirestoreEmulator(db, "localhost", FIRESTORE_EMULATOR_PORT); connectAuthEmulator(auth, `http://localhost:${AUTH_EMULATOR_PORT}`); connectStorageEmulator(storage, "localhost", STORAGE_EMULATOR_PORT); connectFunctionsEmulator(functions, "localhost", FUNCTIONS_EMULATOR_PORT); } ``` ### Start Emulators ```bash # Install Firebase CLI npm install -g firebase-tools # Initialize project (creates firebase.json) firebase init # Start all emulators with data persistence firebase emulators:start --import=./emulator-data --export-on-exit=./emulator-data # Start specific emulators only firebase emulators:start --only firestore,auth,functions # Run emulators and execute tests, then shut down firebase emulators:exec "npm test" ``` **Why good:** Emulator connections guarded by `NODE_ENV`, named constants for ports, `--import/--export-on-exit` persists emulator data between sessions, `emulators:exec` for CI pipelines --- ## Offline Persistence Enable Firestore offline persistence for apps that need to work without connectivity. ```typescript import { initializeFirestore, persistentLocalCache, persistentMultipleTabManager, } from "firebase/firestore"; import { initializeApp } from "firebase/app"; const app = initializeApp(firebaseConfig); // Enable persistent offline cache with multi-tab support export const db = initializeFirestore(app, { localCache: persistentLocalCache({ tabManager: persistentMultipleTabManager(), }), }); ``` **Why good:** `initializeFirestore` with `persistentLocalCache` is the modern approach (replaces deprecated `enableIndexedDbPersistence`), `persistentMultipleTabManager` enables multi-tab sync, configured at initialization time ```typescript // BAD: Deprecated persistence API import { getFirestore, enableIndexedDbPersistence } from "firebase/firestore"; const db = getFirestore(app); enableIndexedDbPersistence(db); // Deprecated -- use initializeFirestore instead ``` **Why bad:** `enableIndexedDbPersistence` is deprecated, must be called before any other Firestore operations, does not support multi-tab by default --- _For core concepts, see [SKILL.md](../SKILL.md). For quick reference tables, see [reference.md](../reference.md)._ -
storage.md 6.5 KB
# Firebase -- Cloud Storage Examples > File upload with progress tracking, download URLs, deletion, client-side validation, and App Check. See [SKILL.md](../SKILL.md) for core concepts and [reference.md](../reference.md) for quick lookups. **Related examples:** - [core.md](core.md) -- Firebase project setup, emulator suite - [firestore.md](firestore.md) -- Firestore CRUD, queries, real-time listeners - [auth.md](auth.md) -- Authentication flows - [functions.md](functions.md) -- Cloud Functions v2, Admin SDK - [security-rules.md](security-rules.md) -- Firestore and Storage security rules --- ## Storage Upload with Progress and Validation Complete file upload flow with client-side validation, progress tracking, and typed results. ```typescript // services/storage-service.ts import { ref, uploadBytesResumable, getDownloadURL, deleteObject, listAll, type UploadTaskSnapshot, } from "firebase/storage"; import { storage, auth } from "../lib/firebase"; // --- Constants --- const MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB const MAX_DOCUMENT_SIZE_BYTES = 20 * 1024 * 1024; // 20 MB const ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp"]; const ALLOWED_DOCUMENT_TYPES = [ "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ]; // --- Types --- export interface UploadResult { url: string; path: string; } export interface UploadProgress { bytesTransferred: number; totalBytes: number; percent: number; } // --- Validation --- function validateImageFile(file: File): void { if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { throw new Error( `Invalid image type: ${file.type}. Allowed: ${ALLOWED_IMAGE_TYPES.join(", ")}`, ); } if (file.size > MAX_IMAGE_SIZE_BYTES) { throw new Error("Image exceeds 5MB size limit"); } } function validateDocumentFile(file: File): void { if (!ALLOWED_DOCUMENT_TYPES.includes(file.type)) { throw new Error(`Invalid document type: ${file.type}`); } if (file.size > MAX_DOCUMENT_SIZE_BYTES) { throw new Error("Document exceeds 20MB size limit"); } } // --- Upload Functions --- export function uploadImage( file: File, path: string, onProgress?: (progress: UploadProgress) => void, ): Promise<UploadResult> { validateImageFile(file); return uploadFile(file, path, onProgress); } export function uploadDocument( file: File, path: string, onProgress?: (progress: UploadProgress) => void, ): Promise<UploadResult> { validateDocumentFile(file); return uploadFile(file, path, onProgress); } function uploadFile( file: File, path: string, onProgress?: (progress: UploadProgress) => void, ): Promise<UploadResult> { const user = auth.currentUser; if (!user) { throw new Error("Must be authenticated to upload files"); } const storageRef = ref(storage, path); const uploadTask = uploadBytesResumable(storageRef, file, { contentType: file.type, customMetadata: { uploadedBy: user.uid }, }); return new Promise((resolve, reject) => { const PERCENT_MULTIPLIER = 100; uploadTask.on( "state_changed", (snapshot: UploadTaskSnapshot) => { if (onProgress) { onProgress({ bytesTransferred: snapshot.bytesTransferred, totalBytes: snapshot.totalBytes, percent: (snapshot.bytesTransferred / snapshot.totalBytes) * PERCENT_MULTIPLIER, }); } }, (error) => reject(new Error(`Upload failed: ${error.message}`)), async () => { const url = await getDownloadURL(uploadTask.snapshot.ref); resolve({ url, path }); }, ); }); } // --- Delete --- export async function deleteFile(path: string): Promise<void> { const storageRef = ref(storage, path); await deleteObject(storageRef); } // --- List Files --- export async function listUserFiles(userId: string): Promise<string[]> { const listRef = ref(storage, `documents/${userId}`); const result = await listAll(listRef); return result.items.map((item) => item.fullPath); } ``` **Key patterns:** Client-side validation before upload (type and size), resumable upload with progress callback, auth check before upload, `customMetadata` for audit trail, typed upload result with URL and path, separate validation functions per file category --- ## Simple Upload (without progress) For cases where progress tracking is not needed. ```typescript import { ref, uploadBytes, getDownloadURL, deleteObject, type UploadTaskSnapshot, } from "firebase/storage"; import { storage } from "../lib/firebase"; const AVATARS_PATH = "avatars"; const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB // Simple upload async function uploadAvatar(userId: string, file: File): Promise<string> { if (file.size > MAX_FILE_SIZE_BYTES) { throw new Error("File exceeds 5MB limit"); } const storageRef = ref(storage, `${AVATARS_PATH}/${userId}/avatar.png`); await uploadBytes(storageRef, file, { contentType: file.type, customMetadata: { uploadedBy: userId }, }); return getDownloadURL(storageRef); } // Delete a file async function deleteAvatar(userId: string): Promise<void> { const storageRef = ref(storage, `${AVATARS_PATH}/${userId}/avatar.png`); await deleteObject(storageRef); } ``` **Key patterns:** `uploadBytes` for simple non-resumable uploads, `getDownloadURL` for publicly accessible URLs, named constants for paths and limits --- ## App Check Setup Protect your backend resources from abuse by verifying requests come from your app. ```typescript // lib/firebase.ts -- add App Check initialization import { initializeApp } from "firebase/app"; import { initializeAppCheck, ReCaptchaEnterpriseProvider, } from "firebase/app-check"; const app = initializeApp(firebaseConfig); // Enable App Check with reCAPTCHA Enterprise const RECAPTCHA_SITE_KEY = process.env.RECAPTCHA_SITE_KEY!; // Enable debug token in development if (process.env.NODE_ENV === "development") { // @ts-expect-error -- Firebase debug token global self.FIREBASE_APPCHECK_DEBUG_TOKEN = true; } const appCheck = initializeAppCheck(app, { provider: new ReCaptchaEnterpriseProvider(RECAPTCHA_SITE_KEY), isTokenAutoRefreshEnabled: true, }); ``` **Key patterns:** reCAPTCHA Enterprise is the recommended provider (reCAPTCHA v3 is being phased out), debug token in development for emulator support, `isTokenAutoRefreshEnabled` keeps the token fresh, `@ts-expect-error` for the global debug token --- _For core concepts, see [SKILL.md](../SKILL.md). For quick reference tables, see [reference.md](../reference.md)._
-
-
reference.md 12 KB
# Firebase Reference > Firebase CLI commands, project structure, service configuration, and quick lookup tables. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) for code examples. --- ## Firebase CLI Commands ### Project Setup ```bash # Install Firebase CLI npm install -g firebase-tools # Login to Firebase firebase login # Initialize a new project (interactive setup) firebase init # Select services: firestore, functions, hosting, storage, emulators ``` ### Deployment ```bash # Deploy everything firebase deploy # Deploy specific services firebase deploy --only firestore:rules firebase deploy --only functions firebase deploy --only hosting firebase deploy --only storage # Deploy a single function firebase deploy --only functions:myFunctionName # Preview hosting before deploying firebase hosting:channel:deploy preview-channel ``` ### Emulators ```bash # Start all configured emulators firebase emulators:start # Start with data persistence firebase emulators:start --import=./emulator-data --export-on-exit=./emulator-data # Start specific emulators firebase emulators:start --only firestore,auth,functions # Run tests against emulators firebase emulators:exec "npm test" # Open Emulator UI in browser # Default: http://localhost:4000 ``` ### Functions ```bash # Initialize Cloud Functions with TypeScript firebase init functions # Select TypeScript when prompted # Serve functions locally (uses emulator) firebase emulators:start --only functions # View function logs firebase functions:log # Delete a deployed function firebase functions:delete myFunctionName ``` --- ## Environment Variables ```bash # Client-side (safe to expose -- use your framework's public env prefix) FIREBASE_API_KEY=AIza... FIREBASE_AUTH_DOMAIN=my-project.firebaseapp.com FIREBASE_PROJECT_ID=my-project FIREBASE_STORAGE_BUCKET=my-project.appspot.com FIREBASE_MESSAGING_SENDER_ID=123456789 FIREBASE_APP_ID=1:123456789:web:abc123 # Server-side only (NEVER expose to client) FIREBASE_SERVICE_ACCOUNT={"type":"service_account",...} ``` **Note:** Firebase API keys are NOT secret -- they identify your project but don't grant access. Security rules protect your data, not the API key. Use your framework's public env prefix (e.g., `NEXT_PUBLIC_`, `VITE_`) when exposing to client code. --- ## Project Structure ``` my-firebase-app/ +-- firebase.json # Firebase project configuration +-- firestore.rules # Firestore security rules +-- firestore.indexes.json # Composite index definitions +-- storage.rules # Storage security rules +-- functions/ | +-- src/ | | +-- index.ts # Cloud Functions entry point | | +-- posts.ts # Post-related functions | | +-- auth.ts # Auth trigger functions | +-- package.json # Functions dependencies | +-- tsconfig.json # TypeScript config for functions +-- src/ | +-- lib/ | | +-- firebase.ts # Firebase client initialization | +-- types/ | | +-- post.ts # Firestore document types +-- emulator-data/ # Persisted emulator state ``` --- ## firebase.json Configuration ```json { "firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json" }, "functions": { "source": "functions", "runtime": "nodejs22" }, "hosting": { "public": "dist", "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], "rewrites": [ { "source": "/api/**", "function": { "functionId": "api", "region": "us-central1" } }, { "source": "**", "destination": "/index.html" } ] }, "storage": { "rules": "storage.rules" }, "emulators": { "auth": { "port": 9099 }, "firestore": { "port": 8080 }, "functions": { "port": 5001 }, "storage": { "port": 9199 }, "hosting": { "port": 5000 }, "ui": { "enabled": true, "port": 4000 } } } ``` --- ## Modular SDK Import Paths | Service | Import Path | Key Exports | | ------------------ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | App | `firebase/app` | `initializeApp`, `getApp`, `getApps` | | Firestore | `firebase/firestore` | `getFirestore`, `collection`, `doc`, `getDocs`, `getDoc`, `setDoc`, `updateDoc`, `deleteDoc`, `onSnapshot`, `query`, `where`, `orderBy`, `limit`, `runTransaction`, `writeBatch` | | Firestore Lite | `firebase/firestore/lite` | Same as above minus `onSnapshot` (smaller bundle) | | Auth | `firebase/auth` | `getAuth`, `createUserWithEmailAndPassword`, `signInWithEmailAndPassword`, `signInWithPopup`, `signOut`, `onAuthStateChanged` | | Storage | `firebase/storage` | `getStorage`, `ref`, `uploadBytes`, `uploadBytesResumable`, `getDownloadURL`, `deleteObject` | | Functions (client) | `firebase/functions` | `getFunctions`, `httpsCallable`, `connectFunctionsEmulator` | --- ## Admin SDK Import Paths | Service | Import Path | Key Exports | | --------- | -------------------------- | ---------------------------------------------------------------------- | | App | `firebase-admin/app` | `initializeApp`, `cert`, `getApp`, `getApps` | | Firestore | `firebase-admin/firestore` | `getFirestore`, `FieldValue`, `Timestamp` | | Auth | `firebase-admin/auth` | `getAuth`, `createCustomToken`, `verifyIdToken`, `setCustomUserClaims` | | Storage | `firebase-admin/storage` | `getStorage` | | Messaging | `firebase-admin/messaging` | `getMessaging`, `send`, `sendMulticast` | --- ## Cloud Functions v2 Import Paths | Trigger Type | Import Path | Key Exports | | --------------- | --------------------------------- | ---------------------------------------------------------------------------------- | | HTTP / Callable | `firebase-functions/v2/https` | `onRequest`, `onCall`, `HttpsError` | | Firestore | `firebase-functions/v2/firestore` | `onDocumentCreated`, `onDocumentUpdated`, `onDocumentDeleted`, `onDocumentWritten` | | Auth | `firebase-functions/v2/identity` | `beforeUserCreated`, `beforeUserSignedIn` | | Scheduler | `firebase-functions/v2/scheduler` | `onSchedule` | | Storage | `firebase-functions/v2/storage` | `onObjectFinalized`, `onObjectDeleted` | | Pub/Sub | `firebase-functions/v2/pubsub` | `onMessagePublished` | | Alerts | `firebase-functions/v2/alerts` | `onAlertPublished` | --- ## Firestore Query Operators | Method | Description | Example | | ------------------------------------------ | --------------------------- | -------------------------------------------------------- | | `where("field", "==", value)` | Equality | `where("published", "==", true)` | | `where("field", "!=", value)` | Not equal | `where("status", "!=", "archived")` | | `where("field", ">", value)` | Greater than | `where("age", ">", 18)` | | `where("field", ">=", value)` | Greater than or equal | `where("price", ">=", 10)` | | `where("field", "<", value)` | Less than | `where("score", "<", 50)` | | `where("field", "<=", value)` | Less than or equal | `where("count", "<=", 100)` | | `where("field", "in", [])` | In array (max 30 values) | `where("status", "in", ["active", "pending"])` | | `where("field", "not-in", [])` | Not in array (max 10) | `where("role", "not-in", ["banned"])` | | `where("field", "array-contains", val)` | Array contains | `where("tags", "array-contains", "firebase")` | | `where("field", "array-contains-any", [])` | Array contains any (max 30) | `where("tags", "array-contains-any", ["web", "mobile"])` | | `orderBy("field", "asc"\|"desc")` | Sort results | `orderBy("createdAt", "desc")` | | `limit(n)` | Limit results | `limit(20)` | | `startAfter(docSnapshot)` | Cursor pagination | `startAfter(lastDoc)` | | `startAt(value)` | Start at value | `startAt("A")` | | `endBefore(value)` | End before value | `endBefore("Z")` | --- ## Firestore Limits | Limit | Value | | -------------------------------------- | -------------- | | Max document size | 1 MB | | Max fields per document | 20,000 | | Max nested depth | 20 levels | | Max writes per batch | 500 | | Max `in` / `array-contains-any` values | 30 | | Max `not-in` values | 10 | | Sustained write rate per document | 1 write/second | | Max composite indexes per database | 200 | | Max single-field index exemptions | 200 | --- ## Auth Events (onAuthStateChanged) | State | `user` Parameter | When | | --------------- | ---------------- | --------------------------------------- | | Initializing | `null` | Page load, before session check | | Signed in | `User` object | After sign-in or session restore | | Signed out | `null` | After sign-out or session expiry | | Token refreshed | `User` (updated) | ID token auto-refreshed (every ~1 hour) | --- ## Package Versions (as of March 2026) | Package | Version | Purpose | | -------------------- | ------- | ------------------------------------------------ | | `firebase` | 12.11.0 | Client SDK (Firestore, Auth, Storage, etc.) | | `firebase-admin` | 13.7.0 | Admin SDK (server-side, bypasses security rules) | | `firebase-functions` | 7.2.2 | Cloud Functions SDK (v2 API) | | `firebase-tools` | latest | Firebase CLI | **Runtime Requirements:** - Node.js 20 or 22 (Node.js 18 deprecated early 2025) - ES2020+ target in tsconfig.json -
SKILL.md 23.9 KB
--- name: api-baas-firebase description: Firebase backend-as-a-service — Firestore, Authentication, Cloud Functions v2, Storage, Hosting, Admin SDK, security rules, emulator suite --- # Firebase Patterns > **Quick Guide:** Use Firebase as your backend-as-a-service for Firestore database, authentication, Cloud Functions, file storage, and hosting. Always use the modular SDK (`firebase/app`, `firebase/firestore`, etc.) for tree-shaking, type Firestore documents with TypeScript interfaces, write security rules for every collection, and use the Admin SDK only on the server. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use the modular Firebase SDK imports (`firebase/app`, `firebase/firestore`, `firebase/auth`) -- NEVER use the deprecated `firebase/compat` namespace API)** **(You MUST write Firestore security rules for EVERY collection -- a collection without rules is wide open in production)** **(You MUST NEVER expose Firebase Admin SDK credentials or service account keys in client-side code)** **(You MUST use Cloud Functions v2 API (`firebase-functions/v2/https`, `firebase-functions/v2/firestore`) -- NOT the deprecated v1 API)** **(You MUST handle all Firestore operations with error checking -- never assume reads/writes succeed)** </critical_requirements> --- **Auto-detection:** Firebase, initializeApp, firebase/app, firebase/firestore, firebase/auth, getFirestore, getAuth, onAuthStateChanged, collection, doc, getDocs, setDoc, updateDoc, deleteDoc, onSnapshot, firebase-admin, firebase-functions, Cloud Functions, Firestore security rules, firebase.json, firebase deploy **When to use:** - Initializing Firebase and configuring services (Firestore, Auth, Storage, Functions) - Implementing authentication (email/password, OAuth, phone, custom tokens) - Querying and writing Firestore documents (CRUD, real-time listeners, transactions) - Writing Cloud Functions v2 (HTTP handlers, callable functions, Firestore triggers, scheduled functions) - Uploading and serving files from Firebase Storage - Deploying to Firebase Hosting with function rewrites - Writing Firestore and Storage security rules - Using the Firebase Admin SDK for server-side operations **Key patterns covered:** - Modular SDK setup with `initializeApp` and service getters (`getFirestore`, `getAuth`, `getStorage`) - Auth flows: sign up, sign in, OAuth, phone auth, `onAuthStateChanged`, session management - Firestore CRUD with `doc()`, `collection()`, `getDocs()`, `setDoc()`, `updateDoc()`, `deleteDoc()` - Firestore real-time listeners with `onSnapshot()` - Firestore queries with `where()`, `orderBy()`, `limit()`, composite indexes - Cloud Functions v2: `onRequest`, `onCall`, `onDocumentCreated`, `onSchedule` - Firebase Admin SDK: `initializeApp()`, `getFirestore()`, `getAuth()`, custom tokens, user management - Security rules: read/write granularity, auth-based access, data validation - Emulator suite for local development and testing - Offline persistence with `persistentLocalCache` **When NOT to use:** - Complex relational queries needing JOIN operations (use a relational database with an ORM) - Full server-side ORM patterns (Firestore is a document database, not relational) - Applications using a non-Firebase authentication provider - Applications requiring complex server-side business logic beyond Cloud Functions scope **Examples:** - [Core Setup & Configuration](examples/core.md) -- App init, emulators, offline persistence - [Firestore Database](examples/firestore.md) -- CRUD, queries, real-time listeners, transactions - [Authentication](examples/auth.md) -- Email/password, OAuth, auth state, profile sync - [Cloud Functions & Admin SDK](examples/functions.md) -- HTTP, callable, triggers, scheduled, Admin SDK - [Cloud Storage](examples/storage.md) -- Upload with progress, validation, App Check - [Security Rules](examples/security-rules.md) -- Firestore and Storage rules patterns --- <philosophy> ## Philosophy Firebase is Google's Backend-as-a-Service platform providing a complete backend through Firestore (document database), Authentication, Cloud Functions, Storage, Hosting, and more. The modular SDK (v12+) uses tree-shakeable ES module imports for minimal bundle sizes. **Core principles:** 1. **Modular imports for tree-shaking** -- Import only what you need from `firebase/firestore`, `firebase/auth`, etc. The modular SDK can reduce bundle size by 80%+ compared to the legacy namespace API. 2. **Document-oriented data model** -- Firestore stores data as documents in collections. Design your data model around your query patterns, not normalized relations. Denormalization is expected. 3. **Security rules are mandatory** -- Firestore and Storage are directly accessible from clients. Security rules are your only server-side access control. Every collection needs rules. 4. **Cloud Functions v2 on Cloud Run** -- 2nd generation functions run on Cloud Run with better scaling, concurrency, longer timeouts (up to 60 minutes), and traffic splitting. Always use v2 for new projects. 5. **Offline-first with persistence** -- Firestore supports offline persistence via IndexedDB. Enable it with `persistentLocalCache` for apps that must work without connectivity. 6. **Admin SDK for server-side** -- The Firebase Admin SDK bypasses security rules and has full access. Use it only in trusted server environments (Cloud Functions, API servers). **When to use Firebase:** - Rapid prototyping and MVPs with auth, database, and storage out of the box - Real-time applications (chat, live dashboards, collaborative editing) via Firestore listeners - Mobile and web apps needing offline support with automatic sync - Projects wanting serverless backend logic with Cloud Functions - Applications needing simple file storage with security rules **When NOT to use:** - Complex relational data with many-to-many relationships and JOINs (use a relational database) - Applications needing full-text search (Firestore has limited query capabilities -- use a dedicated search service) - High-write-throughput scenarios exceeding 10,000 writes/second to a single document - Applications requiring complex server-side transactions spanning multiple services </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Firebase App Initialization (Modular SDK) Initialize Firebase with the modular SDK for tree-shaking. Each service has its own import path. ```typescript // lib/firebase.ts import { initializeApp } from "firebase/app"; import { getFirestore } from "firebase/firestore"; import { getAuth } from "firebase/auth"; import { getStorage } from "firebase/storage"; const app = initializeApp(firebaseConfig); export const db = getFirestore(app); export const auth = getAuth(app); export const storage = getStorage(app); ``` **Why good:** Modular imports enable tree-shaking, each service initialized from the app instance, named exports > Full setup with emulators and offline persistence: [examples/core.md](examples/core.md) --- ### Pattern 2: Firestore CRUD Operations Use modular functions for all Firestore read/write operations. Always type your documents. ```typescript const POSTS_COLLECTION = "posts"; // Read const docSnap = await getDoc(doc(db, POSTS_COLLECTION, postId)); if (!docSnap.exists()) throw new Error(`Not found: ${postId}`); // Create with auto-ID and server timestamp const docRef = doc(collection(db, POSTS_COLLECTION)); await setDoc(docRef, { ...data, createdAt: serverTimestamp() }); // Update specific fields await updateDoc(doc(db, POSTS_COLLECTION, postId), { ...data, updatedAt: serverTimestamp(), }); // Delete await deleteDoc(doc(db, POSTS_COLLECTION, postId)); ``` **Why good:** Named constants for collection names, existence check before accessing data, `serverTimestamp()` for consistent timestamps, typed input parameters > Full CRUD service with pagination: [examples/firestore.md](examples/firestore.md) --- ### Pattern 3: Firestore Real-Time Listeners Subscribe to document and collection changes with `onSnapshot`. Always unsubscribe to prevent memory leaks. ```typescript function subscribeToPublishedPosts( onUpdate: (posts: Post[]) => void, onError: (error: Error) => void, ): Unsubscribe { const q = query( collection(db, POSTS_COLLECTION), where("published", "==", true), orderBy("createdAt", "desc"), ); return onSnapshot( q, (snapshot) => { onUpdate( snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }) as Post), ); }, (error) => { onError(new Error(`Listener failed: ${error.message}`)); }, ); } // Always store and call the unsubscribe function const unsubscribe = subscribeToPublishedPosts(handler, errorHandler); unsubscribe(); // Cleanup when done ``` **Why good:** Returns `Unsubscribe` for cleanup, separate error callback, typed parameters > Real-time chat implementation: [examples/firestore.md](examples/firestore.md) --- ### Pattern 4: Firestore Transactions and Batched Writes Use transactions for atomic read-then-write operations. Use batched writes for multiple writes without reads. ```typescript // Transaction: atomic read-then-write await runTransaction(db, async (transaction) => { const postSnap = await transaction.get(postRef); if (!postSnap.exists()) throw new Error("Not found"); transaction.update(postRef, { likeCount: increment(1) }); transaction.set(likeRef, { createdAt: serverTimestamp() }); }); // Batch: multiple writes (up to 500) const batch = writeBatch(db); for (const id of postIds.slice(0, MAX_BATCH_SIZE)) { batch.delete(doc(db, POSTS_COLLECTION, id)); } await batch.commit(); ``` **Why good:** Transaction ensures atomicity, `increment()` for safe counters, batch for bulk operations > Full examples: [examples/firestore.md](examples/firestore.md) --- ### Pattern 5: Authentication Flows Use Firebase Authentication with the modular SDK. Handle auth state changes with `onAuthStateChanged`. ```typescript // Sign up / Sign in / Sign out const credential = await createUserWithEmailAndPassword(auth, email, password); const credential = await signInWithEmailAndPassword(auth, email, password); await signOut(auth); // OAuth const result = await signInWithPopup(auth, new GoogleAuthProvider()); // Listen to auth state -- register early in app lifecycle const unsubscribe = onAuthStateChanged(auth, (user) => { /* ... */ }); // Get ID token for API calls const token = await auth.currentUser?.getIdToken(/* forceRefresh */ true); ``` **Why good:** Modular imports, typed `User` return values, `Unsubscribe` for cleanup > Full auth service with profile sync: [examples/auth.md](examples/auth.md) --- ### Pattern 6: Cloud Functions v2 Write Cloud Functions using the v2 API. Import from `firebase-functions/v2/*` subpackages. ```typescript // HTTP function import { onRequest } from "firebase-functions/v2/https"; export const getPosts = onRequest( { cors: true, region: "us-central1" }, async (req, res) => { /* ... */ }, ); // Callable function (with auth) import { onCall, HttpsError } from "firebase-functions/v2/https"; export const createPost = onCall({ region: "us-central1" }, async (request) => { if (!request.auth) throw new HttpsError("unauthenticated", "Must be signed in"); /* ... */ }); // Firestore trigger import { onDocumentCreated } from "firebase-functions/v2/firestore"; export const onPostCreated = onDocumentCreated( { document: "posts/{postId}", region: "us-central1" }, async (event) => { /* ... */ }, ); // Scheduled function import { onSchedule } from "firebase-functions/v2/scheduler"; export const cleanup = onSchedule( { schedule: "every 24 hours", region: "us-central1" }, async () => { /* ... */ }, ); ``` **Why good:** v2 imports from subpackages, region specified, `HttpsError` for callable error handling > Full Cloud Functions API: [examples/functions.md](examples/functions.md) --- ### Pattern 7: Firebase Admin SDK (Server-Side) Use the Admin SDK in Cloud Functions or trusted server environments. Bypasses all security rules. ```typescript import { initializeApp } from "firebase-admin/app"; import { getFirestore } from "firebase-admin/firestore"; import { getAuth } from "firebase-admin/auth"; initializeApp(); // Default credentials in Cloud Functions export const adminDb = getFirestore(); export const adminAuth = getAuth(); // Custom claims for role-based access await adminAuth.setCustomUserClaims(uid, { admin: true }); // Verify client ID tokens const decoded = await adminAuth.verifyIdToken(idToken); ``` **Why good:** Modular Admin SDK imports, default credentials in Cloud Functions, custom claims for RBAC > Full Admin SDK patterns: [examples/functions.md](examples/functions.md) --- ### Pattern 8: Firebase Storage Upload, download, and manage files with Firebase Storage. ```typescript import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage"; const storageRef = ref(storage, `avatars/${userId}/avatar.png`); const uploadTask = uploadBytesResumable(storageRef, file, { contentType: file.type, customMetadata: { uploadedBy: userId }, }); uploadTask.on("state_changed", (snapshot) => { const PERCENT_MULTIPLIER = 100; const percent = (snapshot.bytesTransferred / snapshot.totalBytes) * PERCENT_MULTIPLIER; onProgress(percent); }); ``` **Why good:** Resumable upload with progress, `customMetadata` for audit trail, named constants > Full upload/download/delete with validation: [examples/storage.md](examples/storage.md) --- ### Pattern 9: Security Rules Firestore and Storage security rules are your primary access control mechanism. ``` rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { function isAuthenticated() { return request.auth != null; } function isOwner(userId) { return request.auth.uid == userId; } match /posts/{postId} { allow read: if resource.data.published == true || isOwner(resource.data.authorId); allow create: if isAuthenticated() && request.resource.data.authorId == request.auth.uid; allow update: if isOwner(resource.data.authorId); allow delete: if isOwner(resource.data.authorId); } } } ``` **Why good:** Helper functions, granular CRUD permissions, ownership checks, data validation > Full Firestore + Storage rules: [examples/security-rules.md](examples/security-rules.md) </patterns> --- <performance> ## Performance Optimization ### Query Performance - **Use composite indexes** -- Compound queries (multiple `where` + `orderBy`) need composite indexes. Deploy via `firestore.indexes.json` or let Firestore error messages guide you with auto-generated index links. - **Limit result sets** -- Always use `limit()` to cap query results. Unbounded queries fetch all matching documents. - **Paginate with cursors** -- Use `startAfter()` with the last document snapshot for efficient pagination instead of `offset()`. - **Select specific fields** -- Use `select()` in Admin SDK queries to fetch only needed fields (client SDK always fetches full documents). ### Bundle Size - **Use `initializeAuth` for granular control** -- `getAuth()` enables all auth methods by default. Use `initializeAuth()` with only the providers you need for smaller bundles. - **Use `firebase/firestore/lite`** -- If you don't need real-time listeners, import from `firebase/firestore/lite` for a smaller Firestore bundle (no `onSnapshot`). ### Cloud Functions - **Minimize cold starts** -- Use `onInit()` for lazy initialization. Keep function dependencies small. Consider "fat functions" (one entry point routing to handlers) over many small functions. - **Set appropriate memory/timeout** -- Configure `memory` and `timeoutSeconds` in function options instead of using defaults. - **Use global variables for reusable connections** -- Initialize Admin SDK and database connections outside the function handler so they persist across invocations. </performance> --- <decision_framework> ## Decision Framework ### Firestore vs Realtime Database ``` What does your app need? +-- Complex queries (where, orderBy, compound) --> Firestore +-- Simple key-value lookups with low latency --> Realtime Database +-- Offline support with rich queries --> Firestore +-- Presence system (online/offline status) --> Realtime Database +-- Multi-region availability --> Firestore +-- Very frequent small updates (typing indicators) --> Realtime Database +-- For most new projects --> Firestore (recommended default) ``` ### Auth Method Selection ``` What auth flow does the user need? +-- Email + Password --> createUserWithEmailAndPassword / signInWithEmailAndPassword +-- Social login (Google, GitHub, etc.) --> signInWithPopup / signInWithRedirect +-- Phone + SMS --> signInWithPhoneNumber (requires reCAPTCHA) +-- Email link (passwordless) --> sendSignInLinkToEmail +-- Custom backend auth --> Admin SDK createCustomToken + client signInWithCustomToken +-- Anonymous (guest) --> signInAnonymously (upgrade later with linkWithCredential) ``` ### Cloud Functions: onRequest vs onCall ``` Who is calling the function? +-- External webhooks, third-party services --> onRequest (raw HTTP) +-- Your own client app (web/mobile) --> onCall (automatic auth, input validation) +-- Firestore document changes --> onDocumentCreated / onDocumentUpdated / onDocumentDeleted +-- Scheduled/cron tasks --> onSchedule +-- Authentication events --> onUserCreated / onUserDeleted (from firebase-functions/v2/identity) ``` ### Client SDK vs Admin SDK ``` Where is the code running? +-- Browser / Client-side --> Client SDK (firebase) -- security rules enforced +-- Cloud Functions --> Admin SDK (firebase-admin) -- bypasses security rules +-- API server / backend --> Admin SDK (firebase-admin) -- use service account +-- NEVER use Admin SDK in client code --> It bypasses all security ``` ### Storage: When to Use Firebase Storage ``` What kind of files? +-- User-generated content (avatars, uploads) --> Firebase Storage with security rules +-- Public static assets (CSS, images) --> Firebase Hosting (faster CDN) +-- Large files with progress tracking --> Firebase Storage with uploadBytesResumable +-- Server-generated files (reports, exports) --> Firebase Storage via Admin SDK ``` </decision_framework> --- <integration> ## Integration Guide **Firebase ecosystem integration:** - **Firebase Hosting + Cloud Functions**: Rewrite rules in `firebase.json` route requests to functions - **App Check**: Protect your backend resources from abuse by verifying requests come from your app (`initializeAppCheck` with reCAPTCHA Enterprise) - **Firebase Extensions**: Pre-built Cloud Functions for common tasks (payments, image resizing, email sending) **Client framework integration:** - Use `onAuthStateChanged` in your framework's lifecycle (e.g., context provider, composable, store) to track auth state - Firestore `onSnapshot` listeners require cleanup when components unmount -- use your framework's cleanup mechanism - Wrap Firestore reads in your data fetching solution's query functions for caching and real-time invalidation **Replaces / Conflicts with:** - Other BaaS platforms -- don't use two BaaS solutions for the same purpose - Custom auth providers -- Firebase Auth can replace third-party auth, or vice versa -- don't mix auth providers </integration> --- <red_flags> ## RED FLAGS **High Priority Issues:** - **Wide-open security rules in production** -- The default test-mode rules (`allow read, write: if true`) give everyone full access to your entire database. This is the single most common Firebase security vulnerability. - **Admin SDK credentials in client code** -- Service account keys or Admin SDK imports in browser bundles give attackers full bypass of all security rules. - **Using the compat/namespace API** -- `firebase/compat/*` imports prevent tree-shaking, bundling the entire SDK. The compat layer will be removed in a future major version. - **Not handling auth state changes** -- Calling Firestore without waiting for `onAuthStateChanged` can result in unauthenticated requests that fail silently against security rules. **Medium Priority Issues:** - **Using v1 Cloud Functions API** -- v1 (`functions.https.onRequest`) lacks concurrency, traffic splitting, and longer timeouts. All new functions should use v2 (`firebase-functions/v2/https`). - **Not unsubscribing from `onSnapshot`** -- Leaked listeners keep WebSocket connections open, consume bandwidth, and cause memory leaks. - **Unbounded queries** -- Queries without `limit()` can fetch thousands of documents, causing performance issues and high read costs. - **Monotonically increasing document IDs** -- Sequential IDs like `user1`, `user2` create hotspots. Use Firestore auto-generated IDs or UUIDs. - **Using `functions.config()`** -- Deprecated and will fail after March 2027. Use Cloud Secret Manager (`defineSecret()`) or environment variables. **Common Mistakes:** - **Not adding `.select()` after Admin SDK queries** -- Without `select()`, all document fields are returned, increasing data transfer. - **Calling `getFirestore()` on every operation** -- Initialize once and reuse the instance. Each call creates overhead. - **Missing composite indexes** -- Compound queries fail at runtime if the required composite index doesn't exist. Check error messages for the auto-generated index creation link. - **Deploying without testing security rules** -- Use the emulator to test rules before deploying. Use `firebase emulators:exec "npm test"` in CI. - **Using `enableIndexedDbPersistence`** -- Deprecated. Use `initializeFirestore` with `persistentLocalCache` instead. **Gotchas & Edge Cases:** - **Firestore queries are "all or nothing" with security rules** -- If a query could potentially return documents the user isn't allowed to read, the entire query fails (not just the unauthorized documents). - **Firestore limits** -- 1 MB max document size, 20,000 fields per document, 500 writes per batch, 1 write per second per document sustained. - **Security rules propagation delay** -- Rule updates take up to 1 minute to affect new queries, and up to 10 minutes for active listeners. - **`serverTimestamp()` returns `null` in `onSnapshot` pending writes** -- Until the server confirms the write, the timestamp field is `null` locally. Handle this with `{ serverTimestamps: 'estimate' }` in snapshot options. - **Firebase Hosting has a 60-second timeout** -- Even if your Cloud Function has a longer timeout, requests through Hosting rewrites timeout at 60 seconds. - **`onAuthStateChanged` fires on page load** -- It fires with `null` initially, then with the user if a session exists. Always handle the initial `null` state. - **Firestore `in` queries are limited to 30 values** -- `where("field", "in", array)` supports a maximum of 30 elements in the array (increased from 10 in recent versions). - **Cloud Functions cold starts** -- First invocation after idle has additional latency. Use `onInit()` for lazy initialization and keep dependencies minimal. - **Admin SDK `initializeApp()` should be called once** -- Multiple calls throw an error unless you provide a unique app name. Guard with a try-catch or check `getApps().length`. </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use the modular Firebase SDK imports (`firebase/app`, `firebase/firestore`, `firebase/auth`) -- NEVER use the deprecated `firebase/compat` namespace API)** **(You MUST write Firestore security rules for EVERY collection -- a collection without rules is wide open in production)** **(You MUST NEVER expose Firebase Admin SDK credentials or service account keys in client-side code)** **(You MUST use Cloud Functions v2 API (`firebase-functions/v2/https`, `firebase-functions/v2/firestore`) -- NOT the deprecated v1 API)** **(You MUST handle all Firestore operations with error checking -- never assume reads/writes succeed)** **Failure to follow these rules will create security vulnerabilities, bloated bundles, deprecated code paths, and silent runtime failures.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.