Skip to main content
RapidDev - Software Development Agency
firebase-tutorial

How to Use Firestore Triggers in Firebase Cloud Functions

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.

What you'll learn

  • How to create Firestore triggers using the modular v2 Cloud Functions syntax
  • How to read before/after document data inside trigger handlers
  • How to use wildcards and WithAuthContext variants for user-aware logic
  • How to avoid infinite loops and write idempotent trigger functions
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read15-20 minFirebase Blaze plan, Cloud Functions v2, firebase-functions 4.x+, Node.js 18/20/22March 2026RapidDev Engineering Team
TL;DR

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

1

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

typescript
1firebase init functions
2# Choose TypeScript, select your project
3# 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.

2

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.

typescript
1import { onDocumentCreated } from "firebase-functions/v2/firestore";
2import { logger } from "firebase-functions";
3
4export 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 }
12
13 const orderData = snapshot.data();
14 const orderId = event.params.orderId;
15
16 logger.info(`New order ${orderId}:`, orderData);
17
18 // 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.

3

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.

typescript
1import { onDocumentUpdated } from "firebase-functions/v2/firestore";
2import { logger } from "firebase-functions";
3
4export const onOrderStatusChanged = onDocumentUpdated(
5 "orders/{orderId}",
6 async (event) => {
7 if (!event.data) return;
8
9 const before = event.data.before.data();
10 const after = event.data.after.data();
11
12 // Only act if the status field actually changed
13 if (before.status === after.status) {
14 return;
15 }
16
17 logger.info(
18 `Order ${event.params.orderId} status: ${before.status} → ${after.status}`
19 );
20
21 if (after.status === "shipped") {
22 // Send shipping notification to customer
23 }
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.

4

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.

typescript
1import { onDocumentDeleted } from "firebase-functions/v2/firestore";
2import { getFirestore } from "firebase-admin/firestore";
3import * as admin from "firebase-admin";
4
5admin.initializeApp();
6const db = getFirestore();
7
8export const onUserDeleted = onDocumentDeleted(
9 "users/{userId}",
10 async (event) => {
11 if (!event.data) return;
12
13 const userId = event.params.userId;
14
15 // Clean up the user's posts
16 const postsSnap = await db
17 .collection("posts")
18 .where("authorId", "==", userId)
19 .get();
20
21 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.

5

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.

typescript
1import { onDocumentCreatedWithAuthContext } from "firebase-functions/v2/firestore";
2import { logger } from "firebase-functions";
3
4export const auditOrderCreation = onDocumentCreatedWithAuthContext(
5 "orders/{orderId}",
6 async (event) => {
7 const userId = event.authId; // UID of the user who wrote the doc
8 const authType = event.authType; // "admin" | "user" | "unauthenticated"
9
10 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.

6

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.

typescript
1export const enrichOrder = onDocumentCreated(
2 "orders/{orderId}",
3 async (event) => {
4 if (!event.data) return;
5
6 const data = event.data.data();
7
8 // Guard: skip if already processed by this function
9 if (data.enrichedByFunction === true) {
10 return;
11 }
12
13 // Safe to write back — the guard prevents re-triggering
14 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.

7

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.

typescript
1# Deploy only functions (not hosting, rules, etc.)
2firebase deploy --only functions
3
4# Deploy a specific function
5firebase deploy --only functions:onOrderCreated
6
7# View logs after triggering
8firebase functions:log --only onOrderCreated

Expected result: Functions are deployed and trigger logs appear in the Firebase Console when documents change.

Complete working example

functions/src/index.ts
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";
9
10admin.initializeApp();
11const db = getFirestore();
12
13// Trigger: new order created
14export 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;
20
21 // Guard against infinite loop
22 if (order.processedByFunction) return;
23
24 // Enrich order with computed fields
25 await event.data.ref.update({
26 processedByFunction: true,
27 totalWithTax: order.subtotal * 1.1,
28 createdAt: admin.firestore.FieldValue.serverTimestamp(),
29 });
30
31 logger.info(`Processed new order ${orderId}`);
32 }
33);
34
35// Trigger: order status changed
36export 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();
42
43 if (before.status === after.status) return;
44
45 logger.info(
46 `Order ${event.params.orderId}: ${before.status} → ${after.status}`
47 );
48
49 // Example: update a dashboard counter
50 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);
58
59// Trigger: user deleted — clean up related data
60export const onUserDeleted = onDocumentDeleted(
61 "users/{userId}",
62 async (event) => {
63 if (!event.data) return;
64 const userId = event.params.userId;
65
66 const postsSnap = await db
67 .collection("posts")
68 .where("authorId", "==", userId)
69 .get();
70
71 const batch = db.batch();
72 postsSnap.docs.forEach((doc) => batch.delete(doc.ref));
73 await batch.commit();
74
75 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.

ChatGPT Prompt

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.

Firebase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.