01 — The Ecosystem
Service Selection Guide
GCP + Firebase offers dozens of services. These are the ones that matter for production backend architecture — and exactly when to use each one.
onRequest()onDocumentCreated()onMessagePublished()
WebSocket serverHeavy workloadsLong tasks
onSnapshot()TransactionsSecurity Rules
verifyIdToken()setCustomUserClaims()
topic.publish()onMessagePublished()
createTask()scheduleTime
accessSecretVersion()runWith({secrets})
generateContent()streamGenerateContent()
getSignedUrl()onObjectFinalized()
insertRows()createQueryJob()
onValue()onDisconnect()
WAF rulesRate limiting
02 — Project Structure
Function-first Layout
Organised by domain feature. Each feature exports HTTP functions, Firestore triggers, Pub/Sub subscribers, and scheduled functions. Shared infrastructure in common layers.
03 — Code Patterns
TypeScript Blueprints
Production-ready TypeScript. Every pattern uses the Firebase Admin SDK v12+ and Cloud Functions v2 APIs.
import { onRequest } from 'firebase-functions/v2/https' import { requireAuth, requireRole } from '../shared/auth.middleware' import { validate } from '../shared/validate' import { ProductService } from './products.service' import { z } from 'zod' const CreateProductSchema = z.object({ name: z.string().min(1).max(200), price: z.number().positive(), sku: z.string().min(1), }) // Cloud Functions v2 — supports concurrency, longer timeouts export const getProducts = onRequest( { region: 'us-central1', memory: '256MiB', minInstances: 1 }, async (req, res) => { const user = await requireAuth(req, res) if (!user) return const products = await ProductService.list({ userId: user.uid, page: Number(req.query.page) || 1, }) res.json({ success: true, data: products }) } ) export const createProduct = onRequest( { region: 'us-central1', memory: '256MiB' }, async (req, res) => { const user = await requireAuth(req, res) if (!user) return if (req.method !== 'POST') { res.status(405).json({ error: 'Method not allowed' }); return } const body = validate(CreateProductSchema, req.body, res) if (!body) return const product = await ProductService.create(body, user.uid) res.status(201).json({ success: true, data: product }) } )
import { Request, Response } from 'firebase-functions/v2/https' import { getAuth } from 'firebase-admin/auth' import { HttpsError } from 'firebase-functions/v2/https' import type { DecodedIdToken } from 'firebase-admin/auth' export async function requireAuth( req: Request, res: Response, ): Promise<DecodedIdToken | null> { const token = req.headers.authorization?.split('Bearer ')[1] if (!token) { res.status(401).json({ error: 'Missing auth token' }) return null } try { // Verifies signature, expiry, revocation const decoded = await getAuth().verifyIdToken(token, true) return decoded } catch { res.status(401).json({ error: 'Invalid or expired token' }) return null } } // RBAC — checks custom claims set via setCustomUserClaims export function requireRole( user: DecodedIdToken, res: Response, ...roles: string[] ): boolean { if (!roles.includes(user.role as string)) { res.status(403).json({ error: 'Insufficient permissions' }) return false } return true }
import { onCall, HttpsError } from 'firebase-functions/v2/https' import { z } from 'zod' const PurchaseSchema = z.object({ productId: z.string(), quantity: z.number().int().positive(), }) // Callable — client SDK handles auth + serialization automatically // No headers, no HTTP — call like a typed function from the client export const purchaseProduct = onCall<z.infer<typeof PurchaseSchema>>( { region: 'us-central1', memory: '512MiB' }, async (request) => { // auth is verified automatically by the SDK if (!request.auth) { throw new HttpsError('unauthenticated', 'Login required') } const input = PurchaseSchema.safeParse(request.data) if (!input.success) { throw new HttpsError('invalid-argument', input.error.message) } return OrderService.purchase( input.data, request.auth.uid ) } ) // Client (Flutter, React, etc.): // const fn = httpsCallable(functions, 'purchaseProduct') // const result = await fn({ productId, quantity })
import { onDocumentCreated, onDocumentUpdated, onDocumentDeleted, } from 'firebase-functions/v2/firestore' import { publishEvent } from '../shared/pubsub' import { logger } from '../shared/logger' // Fires when a new product document is created export const onProductCreated = onDocumentCreated( 'products/{productId}', async (event) => { const data = event.data?.data() if (!data) return logger.info('Product created', { id: event.params.productId }) // Publish to Pub/Sub — notif service handles it await publishEvent('product-events', { type: 'product.created', productId: event.params.productId, sellerId: data.sellerId, }) } ) // Fires when any field in the order document changes export const onOrderStatusChanged = onDocumentUpdated( 'orders/{orderId}', async (event) => { const before = event.data?.before.data() const after = event.data?.after.data() if (!before || !after) return // Only react to status field changes if (before.status === after.status) return await publishEvent('order-events', { type: 'order.status_changed', orderId: event.params.orderId, from: before.status, to: after.status, userId: after.userId, }) } )
import { beforeUserCreated, beforeUserSignedIn, } from 'firebase-functions/v2/identity' import { getAuth } from 'firebase-admin/auth' import { getFirestore } from 'firebase-admin/firestore' import { HttpsError } from 'firebase-functions/v2/https' // Block registration from banned domains export const beforeCreate = beforeUserCreated(async (event) => { const email = event.data.email ?? '' const bannedDomains = ['mailinator.com', 'tempmail.com'] if (bannedDomains.some(d => email.endsWith(`@${d}`))) { throw new HttpsError('permission-denied', 'Email domain not allowed') } // Create user profile in Firestore automatically await getFirestore().collection('users').doc(event.data.uid).set({ email, name: event.data.displayName ?? '', role: 'user', createdAt: FieldValue.serverTimestamp(), }) // Return custom claims — attached to JWT immediately return { customClaims: { role: 'user' } } }) // Set RBAC custom claims on sign-in (refresh if role changed) export const beforeSignIn = beforeUserSignedIn(async (event) => { const doc = await getFirestore() .collection('users').doc(event.data.uid).get() const role = doc.data()?.role ?? 'user' return { customClaims: { role } } })
import { onSchedule } from 'firebase-functions/v2/scheduler' import { getFirestore, FieldValue, Timestamp } from 'firebase-admin/firestore' import { logger } from '../shared/logger' // Cloud Scheduler — cron syntax, runs in your region export const cleanupExpiredSessions = onSchedule( { schedule: 'every 6 hours', region: 'us-central1' }, async () => { const db = getFirestore() const now = Timestamp.now() // Firestore bulk delete with batched writes (max 500/batch) const expired = await db.collection('sessions') .where('expiresAt', '<', now) .limit(500) .get() if (expired.empty) return const batch = db.batch() expired.docs.forEach(doc => batch.delete(doc.ref)) await batch.commit() logger.info(`Cleaned ${expired.size} expired sessions`) } )
import { getAuth } from 'firebase-admin/auth' import { logger } from '../shared/logger' export type UserRole = 'admin' | 'moderator' | 'user' export class AuthService { static async setRole(uid: string, role: UserRole): Promise<void> { // Custom claims are embedded in the Firebase JWT // User must refresh their token to see new claims await getAuth().setCustomUserClaims(uid, { role }) logger.info('Role updated', { uid, role }) } static async revokeTokens(uid: string): Promise<void> { await getAuth().revokeRefreshTokens(uid) logger.info('Tokens revoked', { uid }) } static async disableUser(uid: string): Promise<void> { await getAuth().updateUser(uid, { disabled: true }) await AuthService.revokeTokens(uid) } static async verifyAndDecode( idToken: string, checkRevoked = true, ) { // checkRevoked=true validates against revocation time return getAuth().verifyIdToken(idToken, checkRevoked) } }
import { SecretManagerServiceClient } from '@google-cloud/secret-manager' const client = new SecretManagerServiceClient() const cache = new Map<string, string>() export async function getSecret( name: string, version = 'latest', ): Promise<string> { // Cache in memory for function lifetime (warm instance) if (cache.has(name)) return cache.get(name)! const projectId = process.env.GCLOUD_PROJECT const [secret] = await client.accessSecretVersion({ name: `projects/${projectId}/secrets/${name}/versions/${version}`, }) const value = secret.payload?.data?.toString() ?? '' cache.set(name, value) return value } // Alternative — bind secrets as env vars in function config: // export const myFn = onRequest( // { secrets: ['STRIPE_KEY', 'SENDGRID_KEY'] }, // async (req, res) => { // const key = process.env.STRIPE_KEY // auto-injected // } // )
import { PubSub } from '@google-cloud/pubsub' const pubsub = new PubSub() // Type-safe event publishing export async function publishEvent<T extends object>( topicName: string, event: T, attributes: Record<string, string> = {}, ): Promise<string> { const topic = pubsub.topic(topicName) const payload = Buffer.from(JSON.stringify(event)) return topic.publish(payload, { ...attributes, publishedAt: new Date().toISOString(), }) } // Subscriber in notifications/subscribers.ts: import { onMessagePublished } from 'firebase-functions/v2/pubsub' export const onProductEvent = onMessagePublished( { topic: 'product-events', region: 'us-central1' }, async (event) => { const data = event.data.message.json as { type: string; productId: string; sellerId: string } if (data.type === 'product.created') { await NotificationService.notifySeller(data.sellerId) } } )
import { CloudTasksClient } from '@google-cloud/tasks' const tasksClient = new CloudTasksClient() export async function scheduleTask<T>(opts: { queue: string handlerUrl: string payload: T delaySeconds?: number dedupId?: string }) { const project = process.env.GCLOUD_PROJECT! const location = 'us-central1' const parent = tasksClient.queuePath(project, location, opts.queue) const task: any = { httpRequest: { httpMethod: 'POST', url: opts.handlerUrl, headers: { 'Content-Type': 'application/json' }, body: Buffer.from(JSON.stringify(opts.payload)).toString('base64'), oidcToken: { serviceAccountEmail: process.env.SERVICE_ACCOUNT! }, }, } if (opts.delaySeconds) { task.scheduleTime = { seconds: Date.now() / 1000 + opts.delaySeconds } } if (opts.dedupId) task.name = `${parent}/tasks/${opts.dedupId}` return tasksClient.createTask({ parent, task }) }
import { getFirestore, FieldValue, Timestamp, type DocumentData, type QuerySnapshot, } from 'firebase-admin/firestore' import type { Product, CreateProductInput } from './products.types' const db = () => getFirestore() export class ProductRepository { static col() { return db().collection('products') } static async findById(id: string): Promise<Product | null> { const doc = await ProductRepository.col().doc(id).get() return doc.exists ? ProductRepository.fromDoc(doc) : null } static async create( data: CreateProductInput, uid: string ): Promise<Product> { const ref = ProductRepository.col().doc() const doc = { ...data, id: ref.id, sellerId: uid, createdAt: FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(), } await ref.set(doc) return { ...doc, id: ref.id } as Product } static async listBySeller( sellerId: string, opts: { limit?: number; startAfter?: Timestamp } = {}, ): Promise<Product[]> { let q = ProductRepository.col() .where('sellerId', '==', sellerId) .orderBy('createdAt', 'desc') .limit(opts.limit ?? 20) if (opts.startAfter) q = q.startAfter(opts.startAfter) const snap = await q.get() return snap.docs.map(ProductRepository.fromDoc) } static fromDoc = (doc: DocumentData): Product => ({ id: doc.id, ...doc.data() } as Product) }
import { getFirestore, FieldValue } from 'firebase-admin/firestore' export class OrderService { // Atomic: decrement stock + create order in one transaction static async purchase( productId: string, quantity: number, buyerId: string, ) { const db = getFirestore() return db.runTransaction(async (txn) => { const productRef = db.collection('products').doc(productId) const product = await txn.get(productRef) if (!product.exists) throw new Error('Product not found') const stock = product.data()!.stock if (stock < quantity) throw new Error('Insufficient stock') // Decrement stock atomically txn.update(productRef, { stock: FieldValue.increment(-quantity), }) // Create order document const orderRef = db.collection('orders').doc() txn.set(orderRef, { productId, quantity, buyerId, status: 'pending', createdAt: FieldValue.serverTimestamp(), }) return { orderId: orderRef.id } }) } }
// Firestore Security Rules — enforce at the database level // These run on Google's servers — cannot be bypassed by clients rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // Helper functions function isAuthenticated() { return request.auth != null; } function isOwner(userId) { return isAuthenticated() && request.auth.uid == userId; } function hasRole(role) { return isAuthenticated() && request.auth.token.role == role; } function validProduct() { return request.resource.data.keys().hasAll(['name', 'price']) && request.resource.data.name is string && request.resource.data.price is number && request.resource.data.price > 0; } // Users collection match /users/{userId} { allow read: if isOwner(userId) || hasRole('admin'); allow create: if isOwner(userId); allow update: if isOwner(userId) && !request.resource.data.diff( resource.data).affectedKeys() .hasAny(['role', 'uid']); // can't self-promote allow delete: if hasRole('admin'); } // Products collection match /products/{productId} { allow read: if isAuthenticated(); allow create: if isAuthenticated() && validProduct() && request.resource.data.sellerId == request.auth.uid; allow update: if isOwner(resource.data.sellerId) || hasRole('admin'); allow delete: if isOwner(resource.data.sellerId) || hasRole('admin'); } // Orders — users see own orders, admins see all match /orders/{orderId} { allow read: if isOwner(resource.data.buyerId) || isOwner(resource.data.sellerId) || hasRole('admin'); allow create: if isAuthenticated() && request.resource.data.buyerId == request.auth.uid; allow update: if hasRole('admin'); // only admins update } } }
// storage.rules — enforce file access at the CDN level rules_version = '2'; service firebase.storage { match /b/{bucket}/o { // User profile images — owner read/write, public read match /users/{userId}/avatar/{filename} { allow read: if true; // public CDN allow write: if request.auth != null && request.auth.uid == userId && request.resource.size < 5 * 1024 * 1024 // 5MB && request.resource.contentType.matches('image/.*'); } // Product images match /products/{productId}/{filename} { allow read: if true; allow write: if request.auth != null && request.resource.size < 10 * 1024 * 1024 && request.resource.contentType.matches('image/.*'); } // Private documents — only owner match /private/{userId}/{allPaths=**} { allow read, write: if request.auth != null && request.auth.uid == userId; } } }
import { VertexAI, HarmCategory, HarmBlockThreshold } from '@google-cloud/vertexai' const vertex = new VertexAI({ project: process.env.GCLOUD_PROJECT!, location: 'us-central1', }) const model = vertex.getGenerativeModel({ model: 'gemini-1.5-pro', safetySettings: [ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE }, ], generationConfig: { maxOutputTokens: 2048, temperature: 0.4, topP: 0.8, }, }) export class AiService { static async generateProductDescription( productName: string, category: string, ): Promise<string> { const prompt = `Write a compelling 2-paragraph product description for: ${productName} (Category: ${category}). Tone: professional, benefit-focused. No markdown.` const result = await model.generateContent(prompt) return result.response.candidates?.[0] ?.content.parts[0].text ?? '' } // Streaming — for long responses static async* streamResponse(prompt: string) { const stream = await model.generateContentStream(prompt) for await (const chunk of stream.stream) { const text = chunk.candidates?.[0] ?.content.parts[0].text if (text) yield text } } // Multimodal — image + text static async analyzeProductImage(imageUrl: string) { const result = await model.generateContent([ { inlineData: { mimeType: 'image/jpeg', data: imageUrl } }, 'Describe this product image for a marketplace listing.', ]) return result.response.candidates?.[0]?.content.parts[0].text } }
// Streaming Gemini response via Cloud Run (Cloud Functions // has 60s timeout — Cloud Run supports longer streaming) import express from 'express' import { AiService } from './ai.service' import { requireAuth } from '../shared/auth.middleware' export const streamAiResponse = async ( req: express.Request, res: express.Response, ) => { const user = await requireAuth(req, res) if (!user) return const { prompt } = req.body if (!prompt?.trim()) { res.status(400).json({ error: 'Prompt required' }); return } // SSE streaming — client reads chunks as they arrive res.setHeader('Content-Type', 'text/event-stream') res.setHeader('Cache-Control', 'no-cache') res.setHeader('Connection', 'keep-alive') for await (const chunk of AiService.streamResponse(prompt)) { res.write(`data: ${JSON.stringify({ text: chunk })}\n\n`) } res.write('data: [DONE]\n\n') res.end() }
import { initializeApp, getApps } from 'firebase-admin/app' // Initialize once — guard against double-init in warm instances if (!getApps().length) { initializeApp() // auto-configured in GCP — no credentials needed } // Export typed Firestore with converter helper import { getFirestore } from 'firebase-admin/firestore' const db = getFirestore() db.settings({ ignoreUndefinedProperties: true }) export { db } // Typed converter — eliminates manual casting export function converter<T>() { return { toFirestore: (data: T) => data as any, fromFirestore: (snap: any): T => snap.data() as T, } } // Usage: db.collection('products').withConverter(converter<Product>())
import { logger as functionsLogger } from 'firebase-functions' // Firebase Functions logger writes structured JSON to // Google Cloud Logging automatically — no setup needed export const logger = { debug: (msg: string, meta: object = {}) => functionsLogger.debug(msg, meta), info: (msg: string, meta: object = {}) => functionsLogger.info(msg, meta), warn: (msg: string, meta: object = {}) => functionsLogger.warn(msg, meta), error: (msg: string, meta: object = {}) => functionsLogger.error(msg, meta), } // Correlation ID middleware — trace logs across functions import { Request, Response, NextFunction } from 'express' import { randomUUID } from 'crypto' export function correlationMiddleware( req: Request, res: Response, next: NextFunction ) { const traceId = req.headers['x-cloud-trace-context'] as string ?? randomUUID() res.setHeader('x-trace-id', traceId) next() } // package.json scripts: // "deploy": "firebase deploy --only functions" // "deploy:prod": "firebase use prod && firebase deploy" // "emulate": "firebase emulators:start" // "logs": "firebase functions:log"
import { z } from 'zod' import type { Response } from 'express' // Returns typed data or sends 422 and returns null export function validate<T extends z.ZodType>( schema: T, data: unknown, res: Response, ): z.infer<T> | null { const result = schema.safeParse(data) if (!result.success) { res.status(422).json({ success: false, error: 'Validation failed', details: result.error.flatten(), }) return null } return result.data } // Environment config — validated at startup const EnvSchema = z.object({ GCLOUD_PROJECT: z.string(), SERVICE_ACCOUNT: z.string(), ENVIRONMENT: z.enum(['development', 'staging', 'production']), }) export const env = EnvSchema.parse(process.env)
04 — Data Flows
Every Event Traced
Serverless systems are event graphs, not request pipelines. Every interaction triggers a chain of events across managed services.
Authenticated HTTP request → Firestore
Firestore write → event cascade
User registration flow
Image upload → AI processing
Delayed task — Cloud Tasks
Realtime client subscription (no polling)
Gemini streaming response
05 — Feature Matrix
Every Requirement Mapped
All backend requirements from the original brief — translated to serverless GCP + Firebase equivalents.
setCustomUserClaims()verifyIdToken()
runWith({secrets:[]})
Cloud KMSCMEK
publishEvent()scheduleTask()
onSnapshot()onValue()
Cloud Run service
WAF security policy
firestore.rules
generateContent()
onSchedule('every 6 hours')
logger.info()Cloud Monitoring
firebase emulators:start
06 — SDK Registry
The npm Registry
Every package you need to build a complete GCP + Firebase backend. Pinned to production-stable versions.
"engines": {"node": "20"} in package.json. Always specify the runtime explicitly — default may change.
| Package | Version | Purpose | Layer |
|---|---|---|---|
firebase-admin | ^12.x | Admin SDK — Firestore, Auth, Storage, FCM, Messaging server-side access | Firebase |
firebase-functions | ^6.x | Cloud Functions v2 — onRequest, onCall, onDocumentCreated, onSchedule, onMessagePublished | Functions |
@google-cloud/pubsub | ^4.x | Pub/Sub publisher — create topics, publish messages, manage subscriptions | GCP |
@google-cloud/tasks | ^5.x | Cloud Tasks — create HTTP tasks with delay, dedup, rate limiting | GCP |
@google-cloud/secret-manager | ^5.x | Secret Manager SDK — access versioned secrets at runtime | Security |
@google-cloud/vertexai | ^1.x | Vertex AI + Gemini API — text, multimodal, streaming, embeddings | AI |
@google-cloud/storage | ^7.x | Cloud Storage — signed URLs, bucket management, object operations | GCP |
@google-cloud/bigquery | ^7.x | BigQuery — insert rows, run queries, stream events to analytics warehouse | GCP |
zod | ^3.23 | Runtime schema validation — request body, env vars, Firestore data shapes | Shared |
express | ^4.21 | HTTP server for Cloud Run services — WebSocket, streaming AI responses, batch APIs | HTTP |
firebase-tools | ^13.x | Firebase CLI — deploy functions, manage emulators, rules deployment (dev dep) | dev |
typescript | ^5.6 | TypeScript compiler — strict mode, ESM output targeting Node 20 | dev |
tsx | ^4.x | TypeScript execution — run .ts files directly for scripts and migrations | dev |
vitest | ^2.x | Fast unit testing — test services, repositories, validators against emulators | test |
@firebase/rules-unit-testing | ^3.x | Security Rules testing library — test firestore.rules against emulator with typed assertions | test |
| REPLACED BY GCP MANAGED SERVICES: JWT library → Firebase Auth · Redis → Firestore/RTDB · Celery/RabbitMQ → Pub/Sub + Cloud Tasks · Nginx → Cloud Load Balancer · Let's Encrypt → Managed TLS · Database server → Firestore · Cron daemon → Cloud Scheduler | |||
✦ — AI Scaffold
Generate with AI
Paste into Claude, ChatGPT, Gemini, or any AI agent to scaffold this exact architecture from scratch.
You are a senior GCP + Firebase architect. Scaffold a production-ready full-stack app. Stack: - Frontend: Next.js 14 (App Router) on Firebase Hosting - Auth: Firebase Authentication (Email, Google, Apple) - Database: Cloud Firestore (NoSQL) + Firebase Realtime Database for presence - Functions: Cloud Functions v2 (Node.js 20 / TypeScript) triggered by Firestore/HTTP/PubSub - Storage: Cloud Storage for Firebase - Messaging: Cloud Pub/Sub for async processing - Scheduler: Cloud Scheduler → Pub/Sub → Cloud Functions - AI: Vertex AI (Gemini Pro) via Firebase Extensions - Monitoring: Cloud Logging + Cloud Trace + Error Reporting - IaC: Firebase CLI + Terraform for GCP resources Provide: 1. Firebase project structure (functions/, hosting/, firestore.rules, storage.rules) 2. Firestore security rules for multi-tenant auth 3. Cloud Function patterns (onDocumentCreated, callable, HTTP) 4. Next.js Firebase SDK setup (client + admin SDK) 5. Pub/Sub topic + Cloud Function subscription pattern 6. Firestore composite index definitions 7. GitHub Actions deploy pipeline (functions + hosting) 8. Vertex AI Gemini integration via Cloud Function