Firestore triggers in Cloud Functions v2 let you run server-side code automatically whenever a document is created, updated, or deleted. Use onDocumentCreated, onDocumentUpdated, onDocumentDeleted, or onDocumentWritten from firebase-functions/v2/firestore, specify the document path with wildcards, and deploy with firebase deploy --only functions. Triggers are ideal for sending notifications, syncing denormalized data, and enforcing business logic.
Running Server-Side Logic on Firestore Document Changes
Firestore triggers connect your database to Cloud Functions so that server-side code executes automatically on every document create, update, or delete. This tutorial walks you through writing v2 triggers with the modular SDK, accessing event data, deploying, and avoiding the most common pitfalls like infinite loops and non-idempotent handlers.
Prerequisites
- A Firebase project on the Blaze (pay-as-you-go) plan — Cloud Functions require Blaze
- Firebase CLI installed and authenticated (npm install -g firebase-tools && firebase login)
- A functions directory initialized with firebase init functions (TypeScript recommended)
- Basic familiarity with Firestore document/collection model
Step-by-step guide
Initialize your Cloud Functions project
Initialize your Cloud Functions project
If you have not already, run firebase init functions in your project root. Select TypeScript as the language, and choose your Firebase project. This creates a functions/ directory with package.json, tsconfig.json, and src/index.ts. Make sure your package.json has engines set to a supported Node.js version (18, 20, or 22).
1firebase init functions2# Choose TypeScript, select your project3# Verify engines in functions/package.json:4# "engines": { "node": "20" }Expected result: A functions/ directory exists with src/index.ts ready for your trigger code.
Write an onDocumentCreated trigger
Write an onDocumentCreated trigger
Import onDocumentCreated from firebase-functions/v2/firestore and export a function that runs whenever a new document is added to a collection. The event object contains the new document snapshot. Use event.data to read the created document's fields and event.params to access wildcard path segments.
1import { onDocumentCreated } from "firebase-functions/v2/firestore";2import { logger } from "firebase-functions";34export const onOrderCreated = onDocumentCreated(5 "orders/{orderId}",6 async (event) => {7 const snapshot = event.data;8 if (!snapshot) {9 logger.warn("No data in event");10 return;11 }1213 const orderData = snapshot.data();14 const orderId = event.params.orderId;1516 logger.info(`New order ${orderId}:`, orderData);1718 // Example: send a notification, update inventory, etc.19 }20);Expected result: The function triggers every time a new document is added to the orders collection.
Write an onDocumentUpdated trigger
Write an onDocumentUpdated trigger
The onDocumentUpdated trigger fires when an existing document changes. The event contains event.data.before (the old snapshot) and event.data.after (the new snapshot). Compare the two to determine what changed and take action only on meaningful changes. This prevents unnecessary work when unrelated fields update.
1import { onDocumentUpdated } from "firebase-functions/v2/firestore";2import { logger } from "firebase-functions";34export const onOrderStatusChanged = onDocumentUpdated(5 "orders/{orderId}",6 async (event) => {7 if (!event.data) return;89 const before = event.data.before.data();10 const after = event.data.after.data();1112 // Only act if the status field actually changed13 if (before.status === after.status) {14 return;15 }1617 logger.info(18 `Order ${event.params.orderId} status: ${before.status} → ${after.status}`19 );2021 if (after.status === "shipped") {22 // Send shipping notification to customer23 }24 }25);Expected result: The function fires only when a document in orders is updated, and your logic runs only when the status field changes.
Write an onDocumentDeleted trigger
Write an onDocumentDeleted trigger
Use onDocumentDeleted to clean up related data when a document is removed. The event.data contains the snapshot of the deleted document, so you can read its fields to determine what else needs cleaning up — like removing subcollection data or updating counters.
1import { onDocumentDeleted } from "firebase-functions/v2/firestore";2import { getFirestore } from "firebase-admin/firestore";3import * as admin from "firebase-admin";45admin.initializeApp();6const db = getFirestore();78export const onUserDeleted = onDocumentDeleted(9 "users/{userId}",10 async (event) => {11 if (!event.data) return;1213 const userId = event.params.userId;1415 // Clean up the user's posts16 const postsSnap = await db17 .collection("posts")18 .where("authorId", "==", userId)19 .get();2021 const batch = db.batch();22 postsSnap.docs.forEach((doc) => batch.delete(doc.ref));23 await batch.commit();24 }25);Expected result: When a user document is deleted, all their posts are automatically cleaned up.
Use WithAuthContext for user-aware triggers
Use WithAuthContext for user-aware triggers
The WithAuthContext variants (onDocumentCreatedWithAuthContext, etc.) include authentication information about who made the change. This is useful for audit logging or enforcing server-side business rules based on the user who triggered the write.
1import { onDocumentCreatedWithAuthContext } from "firebase-functions/v2/firestore";2import { logger } from "firebase-functions";34export const auditOrderCreation = onDocumentCreatedWithAuthContext(5 "orders/{orderId}",6 async (event) => {7 const userId = event.authId; // UID of the user who wrote the doc8 const authType = event.authType; // "admin" | "user" | "unauthenticated"910 logger.info(11 `Order ${event.params.orderId} created by ${userId} (${authType})`12 );13 }14);Expected result: The trigger logs which user created the document, enabling audit trails.
Prevent infinite loops
Prevent infinite loops
The most dangerous Firestore trigger mistake is writing back to the same document or collection that triggered the function, creating an infinite loop that burns through Cloud Functions invocations and Firestore writes. Prevent this by adding a guard field (like updatedByFunction: true) or by checking whether the change was made by your function before writing again.
1export const enrichOrder = onDocumentCreated(2 "orders/{orderId}",3 async (event) => {4 if (!event.data) return;56 const data = event.data.data();78 // Guard: skip if already processed by this function9 if (data.enrichedByFunction === true) {10 return;11 }1213 // Safe to write back — the guard prevents re-triggering14 await event.data.ref.update({15 enrichedByFunction: true,16 totalWithTax: data.subtotal * 1.1,17 processedAt: admin.firestore.FieldValue.serverTimestamp(),18 });19 }20);Expected result: The function enriches the order once and skips on subsequent invocations thanks to the guard field.
Deploy and test your triggers
Deploy and test your triggers
Deploy your functions with the Firebase CLI. After deployment, create, update, or delete a document in the Firebase Console or from your app to verify the trigger fires. Check the Cloud Functions logs in the Firebase Console under Functions > Logs to see your logger output.
1# Deploy only functions (not hosting, rules, etc.)2firebase deploy --only functions34# Deploy a specific function5firebase deploy --only functions:onOrderCreated67# View logs after triggering8firebase functions:log --only onOrderCreatedExpected result: Functions are deployed and trigger logs appear in the Firebase Console when documents change.
Complete working example
1import * as admin from "firebase-admin";2import { logger } from "firebase-functions";3import {4 onDocumentCreated,5 onDocumentUpdated,6 onDocumentDeleted,7} from "firebase-functions/v2/firestore";8import { getFirestore } from "firebase-admin/firestore";910admin.initializeApp();11const db = getFirestore();1213// Trigger: new order created14export const onOrderCreated = onDocumentCreated(15 "orders/{orderId}",16 async (event) => {17 if (!event.data) return;18 const order = event.data.data();19 const orderId = event.params.orderId;2021 // Guard against infinite loop22 if (order.processedByFunction) return;2324 // Enrich order with computed fields25 await event.data.ref.update({26 processedByFunction: true,27 totalWithTax: order.subtotal * 1.1,28 createdAt: admin.firestore.FieldValue.serverTimestamp(),29 });3031 logger.info(`Processed new order ${orderId}`);32 }33);3435// Trigger: order status changed36export const onOrderStatusChanged = onDocumentUpdated(37 "orders/{orderId}",38 async (event) => {39 if (!event.data) return;40 const before = event.data.before.data();41 const after = event.data.after.data();4243 if (before.status === after.status) return;4445 logger.info(46 `Order ${event.params.orderId}: ${before.status} → ${after.status}`47 );4849 // Example: update a dashboard counter50 if (after.status === "completed") {51 const statsRef = db.doc("stats/orders");52 await statsRef.update({53 completedCount: admin.firestore.FieldValue.increment(1),54 });55 }56 }57);5859// Trigger: user deleted — clean up related data60export const onUserDeleted = onDocumentDeleted(61 "users/{userId}",62 async (event) => {63 if (!event.data) return;64 const userId = event.params.userId;6566 const postsSnap = await db67 .collection("posts")68 .where("authorId", "==", userId)69 .get();7071 const batch = db.batch();72 postsSnap.docs.forEach((doc) => batch.delete(doc.ref));73 await batch.commit();7475 logger.info(`Cleaned up ${postsSnap.size} posts for user ${userId}`);76 }77);Common mistakes when using Firestore Triggers in Firebase Cloud Functions
Why it's a problem: Writing back to the same document that triggered the function without a guard, causing an infinite loop
How to avoid: Add a boolean guard field (e.g., processedByFunction: true) and check it at the top of your handler. Return early if the guard is already set.
Why it's a problem: Using v1 trigger syntax (functions.firestore.document().onCreate) instead of v2 modular imports
How to avoid: Import from firebase-functions/v2/firestore and use onDocumentCreated, onDocumentUpdated, etc. The v2 API supports concurrency, longer timeouts, and Cloud Run features.
Why it's a problem: Not handling the case where event.data is undefined
How to avoid: Always add an if (!event.data) return guard at the top of every trigger handler. The data can be undefined in rare timing edge cases.
Why it's a problem: Deploying functions on the free Spark plan and getting permission errors
How to avoid: Cloud Functions require the Blaze (pay-as-you-go) plan. Upgrade in the Firebase Console under Billing. The free tier on Blaze still gives you 2 million invocations/month at no cost.
Best practices
- Always use v2 trigger imports from firebase-functions/v2/firestore for new functions — v2 supports concurrency and longer timeouts
- Test all triggers locally with firebase emulators:start before deploying to avoid unexpected Blaze charges
- Make trigger functions idempotent — the same event may be delivered more than once in rare cases
- Compare before and after data in onDocumentUpdated to avoid unnecessary work on irrelevant field changes
- Deploy functions selectively with firebase deploy --only functions:functionName to speed up deployments
- Use logger from firebase-functions instead of console.log for structured, severity-tagged Cloud Logging output
- Set appropriate memory and timeout options for triggers that process large amounts of data
- Keep trigger functions focused on one responsibility — split complex logic into separate functions
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to write a Cloud Functions v2 Firestore trigger that runs when a new document is created in a specific collection. Show me the complete TypeScript code using onDocumentCreated from firebase-functions/v2/firestore, including how to read the document data and avoid infinite loops.
Create a Firebase Cloud Function using v2 Firestore triggers. Use onDocumentCreated from firebase-functions/v2/firestore for the orders collection. Read the new document data from event.data, add a guard field to prevent infinite loops, and deploy with firebase deploy --only functions.
Frequently asked questions
Do Firestore triggers work on the free Spark plan?
No. Cloud Functions require the Blaze (pay-as-you-go) plan. However, the Blaze plan includes a generous free tier of 2 million function invocations per month, so low-traffic triggers cost nothing beyond the plan upgrade.
Can a Firestore trigger cause an infinite loop?
Yes. If your trigger writes back to the same document or collection it listens to, it will re-trigger itself indefinitely. This can generate thousands of dollars in charges within minutes. Always add a guard field or check to prevent re-processing.
What is the difference between onDocumentWritten and onDocumentUpdated?
onDocumentWritten fires on create, update, and delete — every possible change. onDocumentUpdated fires only when an existing document is modified, not on creates or deletes. Use onDocumentWritten when you need to handle all three events in one handler.
How long can a Firestore trigger function run before timing out?
Event-driven triggers (including Firestore triggers) have a maximum timeout of 540 seconds (9 minutes) on both v1 and v2. The default is 60 seconds. You can increase it in the function options.
Are Firestore triggers guaranteed to fire exactly once?
No. Cloud Functions provides at-least-once delivery, meaning a trigger may fire more than once for the same event in rare cases. Always write idempotent handlers that produce the same result even if executed multiple times.
Can I trigger a function on a subcollection document?
Yes. Use the full path with wildcards, like onDocumentCreated('users/{userId}/orders/{orderId}', handler). You can access both userId and orderId from event.params.
Should I use v1 or v2 Firestore triggers for new projects?
Always use v2 for new projects. V2 triggers are built on Cloud Run, support concurrent request handling, offer longer timeouts for HTTP functions, and provide larger instance sizes. V1 and v2 can coexist in the same project if you have legacy functions.
How do I test Firestore triggers locally without deploying?
Use the Firebase Emulator Suite. Run firebase emulators:start to start local Firestore and Functions emulators. Your triggers will fire locally when you write to the emulated Firestore, letting you test without Blaze charges.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation