Firebase
Purpose
Architect serverless backends on Firebase — Firestore document modeling, Authentication providers, Cloud Functions triggers, Storage security, Hosting configuration, and operational best practices.
Agent Protocol
Trigger
User request includes: Firebase, Firestore, Firebase Auth, Cloud Functions, Firebase Storage, Firebase Hosting, security rules, Firebase Admin, Firebase Extensions, Firebase App Check, Firebase Remote Config, Firebase emulator, Firebase cost.
Input Context
- All Firebase capabilities needed (DB, auth, storage, functions, hosting)
- Target platforms (web, iOS, Android, Node.js admin)
- Estimated user/request scale
- Auth providers (email, Google, Apple, custom)
Output Artifact
Firebase project architecture, security rules, data model, function triggers, hosting config.
Response Format
Produce artifact directly. No preamble, no postamble, no explanations. No filler, no hedging, no transitions. Strip articles a/an/the where unambiguous. Compress output — why use many token when few do trick.
Completion Criteria
- Firestore data model designed (collections, documents, indexes, subcollections)
- Security rules defined (read/write/validate per collection)
- Auth providers configured (web + mobile SDK setup)
- Cloud Functions triggers mapped (onCreate, onUpdate, onDelete, onRequest, scheduled)
- Storage security rules + Hosting config (rewrites, headers, redirects)
- Cost optimization plan (read/write counts, indexes, function timeout/memory)
Max Response Length
4096 tokens
Decision Tree
Firestore vs Realtime Database?
What data access pattern do you need?
├── Complex queries, filtering, sorting, transactions
│ └── Firestore — rich querying, multi-collection, auto-scaling
├── Low-latency real-time sync, simple key-value data
│ └── Realtime Database — lower latency, simpler pricing (bandwidth), WebSocket-native
└── I need both
└── Use Firestore for structured data, RTDB for presence/live cursors
Data Model: Subcollection vs Top-Level?
How does this data relate and grow?
├── 1:few relationship (<100 items, bounded), always accessed together
│ └── Subcollection — query within parent document context
├── 1:many relationship (unbounded), independent queries needed
│ └── Top-level collection — separate queries, no parent dependency
└── Many:many or cross-collection needs
└── Top-level collection + composite indexes
Cloud Function Trigger Location?
Where in the data lifecycle?
├── After document write → onCreate, onUpdate, onDelete
├── On HTTP request → onRequest (Express-style handler)
├── On schedule → pubsub.schedule (cron-like)
├── On auth event → functions.auth.user().onCreate
└── On storage event → functions.storage.object().onFinalize
Workflow
Step 1: Project Setup
npm install firebase firebase-admin
npm install -g firebase-tools
firebase login
firebase init
# Select: Firestore, Functions, Storage, Hosting, Emulators
// src/lib/firebase.ts (client SDK)
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { getAuth } from 'firebase/auth';
import { getStorage } from 'firebase/storage';
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID!,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
};
const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
export const auth = getAuth(app);
export const storage = getStorage(app);
// Admin SDK (server-side)
import { initializeApp as initializeAdmin, cert } from 'firebase-admin/app';
import { getFirestore as getAdminFirestore } from 'firebase-admin/firestore';
import { getAuth as getAdminAuth } from 'firebase-admin/auth';
const adminApp = initializeAdmin({ credential: cert(serviceAccount) });
export const adminDb = getAdminFirestore(adminApp);
export const adminAuth = getAdminAuth(adminApp);
Step 2: Firestore Data Modeling
Collection: users/{userId}
name, email, avatar, createdAt, role
Collection: posts/{postId}
title, content, authorId, tags[], published, likeCount, createdAt
Subcollection: comments/{commentId}
authorId, content, createdAt
Collection: tags/{tagId}
name, postCount
Step 3: Security Rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isAuth() { return request.auth != null; }
function isOwner(userId) { return request.auth.uid == userId; }
function isAdmin() { return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin'; }
match /users/{userId} {
allow read: if true;
allow write: if isOwner(userId) || isAdmin();
allow create: if isAuth() && request.resource.data.email is string;
}
match /posts/{postId} {
allow read: if true;
allow create: if isAuth() && request.resource.data.authorId == request.auth.uid;
allow update, delete: if isOwner(resource.data.authorId) || isAdmin();
}
match /posts/{postId}/comments/{commentId} {
allow read: if true;
allow write: if isAuth();
}
}
}
service firebase.storage {
match /b/{bucket}/o {
match /users/{userId}/{allPaths=**} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
}
}
Step 4: Cloud Functions
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
export const
.document('posts/{postId}')
.onCreate(async (snap, context) => {
const post = snap.data();
await admin.firestore().doc(`users/${post.authorId}`).update({
postCount: admin.firestore.FieldValue.increment(1),
});
});
export const scheduledCleanup = functions.pubsub
.schedule('every 24 hours')
.onRun(async () => {
const old = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
const snaps = await admin.firestore()
.collection('posts')
.where('createdAt', '<', old)
.where('published', '==', false)
.get();
const batch = admin.firestore().batch();
snaps.forEach(doc => batch.delete(doc.ref));
await batch.commit();
});
Step 5: Authentication
// Email/password sign up
import { createUserWithEmailAndPassword } from 'firebase/auth';
await createUserWithEmailAndPassword(auth, email, password);
// Google sign in
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
const provider = new GoogleAuthProvider();
await signInWithPopup(auth, provider);
// Custom claims (Admin SDK)
await adminAuth.setCustomUserClaims(uid, { role: 'admin' });
// Verify ID token
const decoded = await adminAuth.verifyIdToken(idToken);
if (decoded.role === 'admin') { /* allow */ }
Step 6: Firestore Query Patterns
// Efficient queries — always use existing indexes
const posts = await adminDb
.collection('posts')
.where('published', '==', true)
.where('tags', 'array-contains', 'javascript')
.orderBy('createdAt', 'desc')
.limit(20)
.get();
// Pagination with cursors
const firstPage = await adminDb
.collection('posts')
.orderBy('createdAt', 'desc')
.limit(10)
.get();
const lastVisible = firstPage.docs[firstPage.docs.length - 1];
const secondPage = await adminDb
.collection('posts')
.orderBy('createdAt', 'desc')
.startAfter(lastVisible)
.limit(10)
.get();
// Aggregation (use counters, don't count docs)
async function updatePostCount(userId: string, delta: number) {
await adminDb.runTransaction(async (tx) => {
const ref = adminDb.doc(`counters/${userId}`);
const snap = await tx.get(ref);
const current = snap.data()?.postCount ?? 0;
tx.set(ref, { postCount: current + delta }, { merge: true });
});
}
Step 7: Batched Writes and Transactions
// Batched write (atomic, up to 500 operations)
async function createPostWithTags(post: Post, tagIds: string[]) {
const batch = adminDb.batch();
const postRef = adminDb.collection('posts').doc();
batch.set(postRef, { ...post, createdAt: admin.firestore.FieldValue.serverTimestamp() });
for (const tagId of tagIds) {
const tagRef = adminDb.doc(`tags/${tagId}`);
batch.update(tagRef, { postCount: admin.firestore.FieldValue.increment(1) });
}
await batch.commit();
}
// Transaction (read-then-write, strong consistency)
async function transferPoints(fromId: string, toId: string, amount: number) {
await adminDb.runTransaction(async (tx) => {
const fromRef = adminDb.doc(`users/${fromId}`);
const toRef = adminDb.doc(`users/${toId}`);
const fromSnap = await tx.get(fromRef);
const toSnap = await tx.get(toRef);
if (fromSnap.data()!.points < amount) {
throw new Error('Insufficient points');
}
tx.update(fromRef, { points: admin.firestore.FieldValue.increment(-amount) });
tx.update(toRef, { points: admin.firestore.FieldValue.increment(amount) });
});
}
Step 8: Cost Optimization
| Strategy | Impact | Implementation |
|---|---|---|
| Limit document reads | 90% cost reduction | Paginate, cache, avoid listing all docs |
| Use collection group queries instead of subcollection per doc | Avoid per-doc reads | @firebase/ Firestore collection group indexes |
| Denormalize frequently-read data | Fewer reads | Store user displayName directly in post documents |
| Avoid listening to large collections | Unbounded reads | Always filter with where() clauses |
| Use Firestore bundle for static data | Zero reads for cached data | Generate bundles on schedule, deploy to CDN |
| Shard hot documents at >1 write/s | Avoid contention | Add random suffix to document IDs |
| Set TTL policies | Auto-delete old data | Firestore TTL policy on collections |
Step 9: Firebase Extensions
Use built-in extensions to reduce custom code:
- Resize Images: auto-resize on storage upload
- Translate Text: auto-translate Firestore fields using Cloud Translation
- Trigger Email: send emails from Firestore writes
- Delete User Data: cascade delete when auth user is removed
- Firestore Stripe Subscriptions: manage subscriptions via Firestore writes
Production Considerations
| Concern | Practice |
|---|---|
| Firestore max 1 write/s per doc | Use distributed counters for hot documents |
| Cold start for Functions | Set minInstances for latency-sensitive endpoints. Budget impact: cost per idle instance |
| Function timeouts | Set timeout: 60s for HTTP, 540s for event-driven. Increase for heavy processing |
| Memory allocation | 256MB for light triggers, 1GB+ for image processing, 2GB+ for ML/model loading |
| Emulator suite | Run locally for development. Never develop against production |
| Point-in-time recovery | Enable PITR (7d) for production projects. Additional cost |
| App Check | Enable to block unauthorized client requests. Supports reCAPTCHA, App Attest, SafetyNet |
| Secret management | Store service account JSON in Secret Manager, never in repo |
Security
| Risk | Mitigation |
|---|---|
| Unsecured Firestore | Validate security rules with Firebase emulator. Deploy rules before app |
| Over-permissive read rules | Always scope reads with auth checks. Don't allow read: if true for private data |
| Client-side validation bypass | Security rules validate every request independently |
| Function endpoint abuse | Callable functions verify auth automatically. HTTP functions must validate |
| Storage public access | Use signed URLs with expiry for private files. Block public by default |
| API key exposure | Firebase API keys are public-by-design. Use App Check for additional security |
Anti-Patterns
| Anti-Pattern | Why It's Bad | Fix |
|---|---|---|
| Deeply nested data >20 levels | Cannot query, hard to secure | Use subcollections or top-level collections |
| Counting documents with get() | O(N) reads, expensive | Use distributed counter or maintained count field |
| Listening to entire collection | Unbounded reads and updates | Always use where() filters |
| Reading document in security rule for every access | 1 extra read per access | Use custom claims for role info |
| Functions without error handling | Silent failures, debugging nightmare | Always wrap in try/catch, log errors |
| Huge batched writes (>500 ops) | Batch limit is 500 operations | Split into multiple batches or use more targeted updates |
| Storing arrays for data that grows | Cannot atomically modify large arrays | Use subcollection or map with keys |
| Over-indexing | Each index adds write cost | Only create indexes for actual queries |
Rules
- Firestore: max 1 write per second on a document — use aggregations for counters.
- Use subcollections instead of nested objects when data grows unbounded.
- Security rules evaluate on every read/write — limit get() calls within rules.
- Cloud Functions: set memory/timeout based on workload — 256MB for light triggers, 1GB+ for heavy processing.
- Firebase Pricing: Firestore charges per read/write/delete — index writes count as 1 write + 1 per index.
- Use Firebase App Check to block unauthorized client requests.
- Enable Firestore PITR (point-in-time recovery) for production.
- Functions: use
minInstancesfor latency-sensitive endpoints (cost trade-off). - Store service account JSON outside the repo — use secret manager.
- Every Firebase SDK call from client must be scoped by security rules.
- Denormalize for read performance — data duplication is acceptable in Firestore.
- Always use emulator for development. Never point a client SDK at production Firestore during development.
Implementation Patterns
Firestore Repository
import { Firestore, CollectionReference, DocumentData, Query } from 'firebase/firestore';
interface Entity {
id?: string;
createdAt?: Date;
updatedAt?: Date;
}
class FirestoreRepository<T extends Entity> {
constructor(
private firestore: Firestore,
private collectionPath: string
) {}
get collection(): CollectionReference<T> {
return this.firestore.collection(this.collectionPath) as CollectionReference<T>;
}
async create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<string> {
const docRef = await this.collection.add({
...data,
createdAt: new Date(),
updatedAt: new Date(),
} as any);
return docRef.id;
}
async get(id: string): Promise<T | null> {
const doc = await this.collection.doc(id).get();
return doc.exists ? { id: doc.id, ...doc.data() } as T : null;
}
async update(id: string, data: Partial<Omit<T, 'id'>>): Promise<void> {
await this.collection.doc(id).update({
...data,
updatedAt: new Date(),
} as any);
}
async delete(id: string): Promise<void> {
await this.collection.doc(id).delete();
}
async query(queries: ((ref: CollectionReference) => Query)[]): Promise<T[]> {
let q: Query = this.collection;
for (const queryFn of queries) {
q = queryFn(this.collection);
}
const snapshot = await q.get();
return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }) as T);
}
}
Architecture Decision Trees
Firebase When to Use
Should you use Firebase?
├── Small to medium app (< 100K users)
│ ├── Need real-time sync → Firestore
│ ├── Need auth → Firebase Auth (social providers, email)
│ └── Need file storage → Firebase Storage
│
├── MVP / Prototype
│ └── Firebase = fastest time to ship
│
├── Complex queries, joins, aggregations
│ └── Consider a relational DB (Firestore limits queries)
│
└── Heavy server-side processing
└── Cloud Functions + Firebase or use a backend framework
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Deeply nested collections (>3 levels) | Slow queries, high read cost | Shallow collections with references |
| Missing composite indexes | Queries fail with vague errors | Create indexes when query requires them |
| Storing arrays for growing data | Can't atomically modify large arrays | Use subcollections or maps |
| Not using emulators | Costs accumulate, slow iteration | Firestore emulator for development |
| Functions without error handling | Silent failures | Always wrap in try/catch, log errors |
| Over-indexing | Each index adds write cost | Only index for actual queries |
Performance Optimization
- Batch reads in parallel: Use
getAll()to fetch multiple documents in one batch. Reduces latency from N sequential reads to 1 parallel batch. - Data denormalization for read performance: Duplicate data across documents to avoid joins. Accept data duplication — Firestore charges per read, not per storage.
Implementation Patterns
Observer Pattern for Event Handling
` interface EventObserver { onEvent(event: T): Promise; }
class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `
Configuration-Driven Approach
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
Production Considerations
Deployment Checklist
- Configuration validated against schema before startup
- Health check endpoints registered and monitored
- Graceful shutdown with draining period (30s timeout)
- Resource limits configured (CPU, memory, file descriptors)
- Log level set appropriate for environment
- Metrics endpoint secured and exposed
- Rate limiting configured per-tier
- TLS certificates valid and auto-renewing
- Database migrations run as separate deployment step
- Feature flags ready for gradual rollout
Monitoring and Alerting
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |
Anti-Patterns
| Anti-Pattern | Symptom | Root Cause | Solution |
|---|---|---|---|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |
Performance Optimization
Caching Strategy
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).
Resource Pooling
- Database connections: Pool of reusable connections (HikariCP, pgBouncer)
- HTTP connections: Keep-alive + connection pooling for external calls
- Thread pool: Bounded thread pools for async task execution
Profiling Methodology
- Establish baseline with production traffic profile
- Profile CPU with sampling profiler (pprof, perf, async-profiler)
- Profile memory with heap dumps and allocation tracking
- Profile I/O with strace/perf trace for syscall analysis
- Profile latency with distributed tracing (OpenTelemetry)
- Identify bottleneck, formulate hypothesis, implement fix
- Re-profile to verify improvement, repeat
Security Considerations
Threat Modeling (STRIDE)
- Spoofing: Identity validation, authentication
- Tampering: Integrity checks, digital signatures
- Repudiation: Audit logs, non-repudiation
- Information disclosure: Encryption, access control
- Denial of service: Rate limiting, resource quotas
- Elevation of privilege: Principle of least privilege
Supply Chain Security
- Dependency scanning: Snyk, Dependabot, Trivy
- SBOM generation: CycloneDX or SPDX format
- Signed commits: GPG or SSH commit signing
- Artifact verification: Checksum validation, signature verification
Secrets Management
- Secrets never in code — always in secrets manager (Vault, AWS Secrets Manager)
- Rotation policy: Rotate database credentials every 90 days
- Access audit: Log every secrets access, alert on anomalies
- Encryption at rest and in transit for all secrets
- Principle of least privilege: each service gets only its own secrets
Rules
- Default-deny security posture — allow only explicitly required access.
- All inputs validated, all outputs encoded, all errors handled.
- Defend in depth — multiple layers of security controls.
- Fail securely — errors default to safe behavior.
- Log security-relevant events for audit and investigation.
- Keep dependencies updated — automate vulnerability scanning.
- Design for observability from day one, not as an afterthought.
- Document all architectural decisions with rationale.
- Review code for security, performance, and correctness before merging.