# 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.