# Firebase - Detailed Guide > This file contains the detailed procedure and reference material extracted from `SKILL.md` for focused loading. The root skill defines activation, examples, safety constraints, and limitations. ## Principles - Design data for queries, not relationships - Security rules are mandatory, not optional - Denormalize aggressively - duplication is cheap, joins are expensive - Batch writes and transactions for consistency - Use offline persistence wisely - it's not free - Cloud Functions for what clients shouldn't do - Firebase web configuration is public identifiers, not authorization; never expose Admin/service-account/private provider credentials in client code ## Capabilities - firebase-auth - firestore - firebase-realtime-database - firebase-cloud-functions - firebase-storage - firebase-hosting - firebase-security-rules - firebase-admin-sdk - firebase-emulators ## Scope - general-backend-architecture -> backend - payment-processing -> stripe - email-sending -> email - advanced-auth-flows -> authentication-oauth - kubernetes-deployment -> devops ## Tooling ### Core - firebase - When: Client-side SDK Note: Modular SDK - tree-shakeable - firebase-admin - When: Server-side / Cloud Functions Note: Full access, bypasses security rules - firebase-functions - When: Cloud Functions v2 Note: v2 functions are recommended ### Testing - @firebase/rules-unit-testing - When: Testing security rules Note: Essential - rules bugs are security bugs - firebase-tools - When: Emulator suite Note: Local development without hitting production ### Frameworks - reactfire - When: React + Firebase Note: Hooks-based, handles subscriptions - vuefire - When: Vue + Firebase Note: Vue-specific bindings - angularfire - When: Angular + Firebase Note: Official Angular bindings ## Patterns ### Modular SDK Import Import only what you need for smaller bundles **When to use**: Client-side Firebase usage # MODULAR IMPORTS: """ Firebase v9+ uses modular SDK. Import only what you need. This enables tree-shaking and smaller bundles. """ // WRONG: v8-compat style (larger bundle) import firebase from 'firebase/compat/app'; import 'firebase/compat/firestore'; const db = firebase.firestore(); // RIGHT: v9+ modular (tree-shakeable) import { initializeApp } from 'firebase/app'; import { getFirestore, collection, doc, getDoc } from 'firebase/firestore'; const app = initializeApp(firebaseConfig); const db = getFirestore(app); // Get a document const docRef = doc(db, 'users', 'userId'); const docSnap = await getDoc(docRef); if (docSnap.exists()) { console.log(docSnap.data()); } // Query with constraints import { query, where, orderBy, limit } from 'firebase/firestore'; const q = query( collection(db, 'posts'), where('published', '==', true), orderBy('createdAt', 'desc'), limit(10) ); ### Security Rules Design Secure your data with proper rules from day one **When to use**: Any Firestore database # FIRESTORE SECURITY RULES: """ Rules are your last line of defense. Client reads/writes are evaluated by rules; Admin/server access instead uses IAM and bypasses Firestore rules. Get them wrong, and your data is exposed. """ rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // Helper functions function isSignedIn() { return request.auth != null; } function isOwner(userId) { return request.auth != null && request.auth.uid == userId; } function isAdmin() { return request.auth != null && request.auth.token.admin == true; } // Users collection match /users/{userId} { // Private user record; split public profiles into a separate allowlisted schema. allow read: if isOwner(userId); // Only owner can write their own data allow write: if isOwner(userId); // Private subcollection match /private/{document=**} { allow read, write: if isOwner(userId); } } // Posts collection match /posts/{postId} { // Anyone can read published posts allow read: if resource.data.published == true || isOwner(resource.data.authorId); // Only authenticated users can create allow create: if isSignedIn() && request.resource.data.authorId == request.auth.uid; // Only author can update/delete allow update: if isOwner(resource.data.authorId) && request.resource.data.authorId == resource.data.authorId; allow delete: if isOwner(resource.data.authorId); } // Admin-only collection match /admin/{document=**} { allow read, write: if isAdmin(); } } } ### Data Modeling for Queries Design Firestore data structure around query patterns **When to use**: Designing Firestore schema # FIRESTORE DATA MODELING: """ Firestore is NOT relational. You can't JOIN. Design your data for how you'll QUERY it, not how it relates. """ // WRONG: Normalized (SQL thinking) // users/{userId} // posts/{postId} with authorId field // To get "posts by user" - need to query posts collection // RIGHT: Denormalized for queries // users/{userId}/posts/{postId} - subcollection // OR // posts/{postId} with embedded author data // Document structure for a post const post = { id: 'post123', title: 'My Post', content: '...', // Embed frequently-needed author data author: { id: 'user456', name: 'Jane Doe', avatarUrl: '...' }, // Arrays for IN queries (max 30 items for 'in') tags: ['javascript', 'firebase'], // Maps for compound queries stats: { likes: 42, comments: 7, views: 1000 }, // Timestamps createdAt: serverTimestamp(), updatedAt: serverTimestamp(), // Booleans for filtering published: true, featured: false }; // Query patterns this enables: // - Get post with author info: 1 read (no join needed) // - Posts by tag: where('tags', 'array-contains', 'javascript') // - Featured posts: where('featured', '==', true) // - Recent posts: orderBy('createdAt', 'desc') // When author updates their name, update all their posts // This is the tradeoff: writes are more complex, reads are fast ### Real-time Listeners Subscribe to data changes with proper cleanup **When to use**: Real-time features # REAL-TIME LISTENERS: """ onSnapshot creates a persistent connection. Always unsubscribe when component unmounts to prevent memory leaks and extra reads. """ // React hook for real-time document function useDocument(path) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { setData(null); setLoading(true); setError(null); const docRef = doc(db, path); // Subscribe to document const unsubscribe = onSnapshot( docRef, (snapshot) => { if (snapshot.exists()) { setData({ ...snapshot.data(), id: snapshot.id }); } else { setData(null); } setLoading(false); }, (err) => { setError(err); setLoading(false); } ); // Cleanup on unmount return () => unsubscribe(); }, [path]); return { data, loading, error }; } // Usage function UserProfile({ userId }) { const { data: user, loading } = useDocument(`users/${userId}`); if (loading) return ; return
{user?.name}
; } // Collection with query function usePosts(pageSize = 10) { const [posts, setPosts] = useState([]); useEffect(() => { const q = query( collection(db, 'posts'), where('published', '==', true), orderBy('createdAt', 'desc'), limit(pageSize) ); const unsubscribe = onSnapshot(q, (snapshot) => { const results = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setPosts(results); }); return () => unsubscribe(); }, [pageSize]); return posts; } ### Cloud Functions Patterns Server-side logic with Cloud Functions v2 **When to use**: Backend logic, triggers, scheduled tasks # CLOUD FUNCTIONS V2: """ Cloud Functions run server-side code triggered by events. V2 uses more standard Node.js patterns and better scaling. """ import { onRequest } from 'firebase-functions/v2/https'; import { onDocumentCreated } from 'firebase-functions/v2/firestore'; import { onSchedule } from 'firebase-functions/v2/scheduler'; import { getFirestore, FieldValue } from 'firebase-admin/firestore'; import { getAuth } from 'firebase-admin/auth'; import { initializeApp } from 'firebase-admin/app'; initializeApp(); const db = getFirestore(); // HTTP function export const api = onRequest( { cors: true, region: 'us-central1' }, async (req, res) => { // Verify auth token const token = req.headers.authorization?.split('Bearer ')[1]; if (!token) { res.status(401).json({ error: 'Unauthorized' }); return; } try { const decoded = await getAuth().verifyIdToken(token); // Process request with decoded.uid res.json({ userId: decoded.uid }); } catch (error) { res.status(401).json({ error: 'Invalid token' }); } } ); // Firestore trigger - on document create export const onUserCreated = onDocumentCreated( 'users/{userId}', async (event) => { const snapshot = event.data; const userId = event.params.userId; if (!snapshot) return; const userData = snapshot.data(); // Send welcome email, create related documents, etc. await db.collection('notifications').doc(`welcome-${event.id}`).set({ userId, type: 'welcome', message: `Welcome, ${userData.name}!`, createdAt: FieldValue.serverTimestamp() }); } ); // Scheduled function (every day at midnight) export const dailyCleanup = onSchedule( { schedule: '0 0 * * *', timeZone: 'UTC' }, async (event) => { const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - 30); // Delete old documents const oldDocs = await db.collection('logs') .where('createdAt', '<', cutoff) .limit(500) .get(); const batch = db.batch(); oldDocs.docs.forEach(doc => batch.delete(doc.ref)); await batch.commit(); console.log(`Deleted ${oldDocs.size} old logs`); } ); ### Batch Operations Atomic writes and transactions for consistency **When to use**: Multiple document updates that must succeed together # BATCH WRITES AND TRANSACTIONS: """ Batches: Multiple writes that all succeed or all fail. Transactions: Read-then-write operations with consistency. Operation/request limits depend on SDK/service constraints; check current quotas, document sizes and rules access-call limits. Do not assume one universal transaction count. """ import { writeBatch, runTransaction, doc, getDoc, increment, serverTimestamp } from 'firebase/firestore'; // Batch write - no reads, just writes async function createPostWithTags(post, tags) { const batch = writeBatch(db); // Create post const postRef = doc(collection(db, 'posts')); batch.set(postRef, { ...post, createdAt: serverTimestamp() }); // Update tag counts for (const tag of tags) { const tagRef = doc(db, 'tags', tag); batch.set(tagRef, { count: increment(1), lastUsed: serverTimestamp() }, { merge: true }); } await batch.commit(); return postRef.id; } // Transaction - read and write atomically async function likePost(postId, userId) { return runTransaction(db, async (transaction) => { const postRef = doc(db, 'posts', postId); const likeRef = doc(db, 'posts', postId, 'likes', userId); const postSnap = await transaction.get(postRef); if (!postSnap.exists()) { throw new Error('Post not found'); } const likeSnap = await transaction.get(likeRef); if (likeSnap.exists()) { throw new Error('Already liked'); } // Increment like count and add like document transaction.update(postRef, { likeCount: increment(1) }); transaction.set(likeRef, { userId, createdAt: serverTimestamp() }); return postSnap.data().likeCount + 1; }); } ### Social Login (Google, GitHub, etc.) OAuth provider setup and authentication flows **When to use**: Social login implementation # SOCIAL LOGIN WITH FIREBASE AUTH import { getAuth, signInWithPopup, signInWithRedirect, GoogleAuthProvider, GithubAuthProvider, OAuthProvider } from "firebase/auth"; const auth = getAuth(); // GOOGLE const googleProvider = new GoogleAuthProvider(); googleProvider.addScope("email"); googleProvider.setCustomParameters({ prompt: "select_account" }); async function signInWithGoogle() { try { const result = await signInWithPopup(auth, googleProvider); return result.user; } catch (error) { if (error.code === "auth/account-exists-with-different-credential") { return handleAccountConflict(error); } throw error; } } // GITHUB const githubProvider = new GithubAuthProvider(); githubProvider.addScope("read:user"); // APPLE (requirements depend on app sign-in features and current store policy) const appleProvider = new OAuthProvider("apple.com"); appleProvider.addScope("email"); appleProvider.addScope("name"); ### Popup vs Redirect Auth When to use popup vs redirect for OAuth **When to use**: Choosing authentication flow # Popup: Desktop, SPA (simpler, can be blocked) # Redirect: verify current browser storage/auth-domain configuration; not guaranteed async function signIn(provider) { if (/iPhone|iPad|Android/i.test(navigator.userAgent)) { return signInWithRedirect(auth, provider); } try { return await signInWithPopup(auth, provider); } catch (e) { if (e.code === "auth/popup-blocked") { return signInWithRedirect(auth, provider); } throw e; } } // Check redirect result on page load useEffect(() => { getRedirectResult(auth).then(r => r && setUser(r.user)); }, []); ### Account Linking Link multiple providers to one account **When to use**: User has accounts with different providers import { fetchSignInMethodsForEmail, linkWithCredential } from "firebase/auth"; Account linking requires an explicit user action and recent authenticated session. Do not infer account ownership from email strings or provider-method enumeration; email-enumeration protection can change/disable `fetchSignInMethodsForEmail` behavior. Use the actual provider’s credential extractor, validate non-null credentials, retain them only in short-lived private memory, then authenticate the intended existing account and confirm before linking. Never silently attach credentials to whatever account a popup returns. Keep a verified recovery/sign-in method before unlinking. The required linking UI/error recovery adapter is not bundled. ### Auth State Persistence Control session lifetime **When to use**: Managing user sessions import { setPersistence, browserLocalPersistence, browserSessionPersistence } from "firebase/auth"; // LOCAL: survives browser close (default) // SESSION: cleared on tab close async function signInWithRememberMe(email, pass, remember) { await setPersistence(auth, remember ? browserLocalPersistence : browserSessionPersistence); return signInWithEmailAndPassword(auth, email, pass); } // React auth hook function useAuth() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => onAuthStateChanged(auth, u => { setUser(u); setLoading(false); }), []); return { user, loading }; } ### Email Verification and Password Reset Complete email auth flow **When to use**: Email/password authentication import { sendEmailVerification, sendPasswordResetEmail, reauthenticateWithCredential } from "firebase/auth"; // Sign up with verification async function signUp(email, password) { const result = await createUserWithEmailAndPassword(auth, email, password); await sendEmailVerification(result.user); return result.user; } // Password reset await sendPasswordResetEmail(auth, email); // Change password (requires recent auth) const cred = EmailAuthProvider.credential(user.email, currentPass); await reauthenticateWithCredential(user, cred); await updatePassword(user, newPass); ### Token Management for APIs Handle ID tokens for backend calls **When to use**: Authenticating with backend APIs import { getIdToken, onIdTokenChanged } from "firebase/auth"; // Get token (auto-refreshes if expired) const token = await getIdToken(auth.currentUser); // API helper sketch: only an explicitly approved same-origin relative endpoint. async function apiCall(path, opts = {}) { const url = new URL(path, window.location.origin); if (url.origin !== window.location.origin) throw new Error("Unapproved API origin"); if (!auth.currentUser) throw new Error("Sign-in required"); const token = await getIdToken(auth.currentUser); const headers = new Headers(opts.headers); headers.set("Authorization", "Bearer " + token); return fetch(url, { ...opts, headers, redirect: "error" }); } // Do not blindly replay writes/stream bodies after 401. Reauthentication and retries // require operation idempotency, fresh auth state and explicit failure handling. // SSR sessions: send an ID token once through an HTTPS, CSRF-protected approved // session endpoint; server verifies it and creates a bounded HttpOnly/Secure/SameSite // session cookie using Admin APIs. Never set bearer-token cookies via document.cookie. // Verify session/revocation policy server-side and clear the cookie on logout. // Check admin claim const { claims } = await auth.currentUser.getIdTokenResult(); const isAdmin = claims.admin === true; ## Collaboration ### Delegation Triggers - user needs complex OAuth flow -> authentication-oauth (Firebase Auth handles basics, complex flows need OAuth skill) - user needs payment integration -> stripe (Firebase + Stripe common pattern) - user needs email functionality -> email (Firebase doesn't include email - use SendGrid, Resend, etc.) - user needs container deployment -> devops (Beyond Firebase Hosting - Kubernetes, Docker) - user needs relational data model -> postgres-wizard (Firestore is wrong choice for highly relational data) - user needs full-text search -> elasticsearch-search (Firestore doesn't support full-text search - use Algolia/Elastic) ## Related Skills Works well with: `nextjs-app-router`, `react-patterns`, `authentication-oauth`, `stripe` ## Example integration boundaries These mixed-language snippets are alternatives with imports/UI/schema omitted, not a bundled tested application. Rules need field allowlists/types, server-owned role and counter protection, size bounds and create/update/delete/query tests in the Emulator Suite (including anonymous, owner, other user and admin cases). A query on posts with authorId is valid; denormalization is a tradeoff, not a rule that normalized data is wrong. Rules are not filters: queries must prove their constraints. Backend Admin paths must separately enforce verified identity, object/tenant authorization and IAM. For example, user A updating B’s private user document must fail; A changing a post’s authorId must fail; a retried welcome event must retain one notification. The event-ID set avoids duplicate documents but timestamp replay and external email delivery need transactional idempotency/reconciliation. Scheduled cleanup deletes only one bounded batch here, not all old logs; approve retention/legal holds, dry-run IDs and checkpoint subsequent batches before deployment. Persistent client caches may retain prior-user data on shared devices; reset session-scoped state and verify cache behavior. Budget alerts are not hard spending caps. Do not log full document/user data as the introductory console sketch does in production. No deployment, billing or auth/rules tests were run; verify current Firebase/browser/store requirements before use.