# How to Use Firestore Triggers in Firebase Cloud Functions

- Tool: Firebase
- Difficulty: Intermediate
- Time required: 15-20 min
- Compatibility: Firebase Blaze plan, Cloud Functions v2, firebase-functions 4.x+, Node.js 18/20/22
- Last updated: March 2026

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

## Before you start

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

```
firebase init functions
# Choose TypeScript, select your project
# Verify engines in functions/package.json:
# "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.

```
import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { logger } from "firebase-functions";

export const onOrderCreated = onDocumentCreated(
  "orders/{orderId}",
  async (event) => {
    const snapshot = event.data;
    if (!snapshot) {
      logger.warn("No data in event");
      return;
    }

    const orderData = snapshot.data();
    const orderId = event.params.orderId;

    logger.info(`New order ${orderId}:`, orderData);

    // Example: send a notification, update inventory, etc.
  }
);
```

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

```
import { onDocumentUpdated } from "firebase-functions/v2/firestore";
import { logger } from "firebase-functions";

export const onOrderStatusChanged = onDocumentUpdated(
  "orders/{orderId}",
  async (event) => {
    if (!event.data) return;

    const before = event.data.before.data();
    const after = event.data.after.data();

    // Only act if the status field actually changed
    if (before.status === after.status) {
      return;
    }

    logger.info(
      `Order ${event.params.orderId} status: ${before.status} → ${after.status}`
    );

    if (after.status === "shipped") {
      // Send shipping notification to customer
    }
  }
);
```

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

```
import { onDocumentDeleted } from "firebase-functions/v2/firestore";
import { getFirestore } from "firebase-admin/firestore";
import * as admin from "firebase-admin";

admin.initializeApp();
const db = getFirestore();

export const onUserDeleted = onDocumentDeleted(
  "users/{userId}",
  async (event) => {
    if (!event.data) return;

    const userId = event.params.userId;

    // Clean up the user's posts
    const postsSnap = await db
      .collection("posts")
      .where("authorId", "==", userId)
      .get();

    const batch = db.batch();
    postsSnap.docs.forEach((doc) => batch.delete(doc.ref));
    await batch.commit();
  }
);
```

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

```
import { onDocumentCreatedWithAuthContext } from "firebase-functions/v2/firestore";
import { logger } from "firebase-functions";

export const auditOrderCreation = onDocumentCreatedWithAuthContext(
  "orders/{orderId}",
  async (event) => {
    const userId = event.authId;    // UID of the user who wrote the doc
    const authType = event.authType; // "admin" | "user" | "unauthenticated"

    logger.info(
      `Order ${event.params.orderId} created by ${userId} (${authType})`
    );
  }
);
```

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

```
export const enrichOrder = onDocumentCreated(
  "orders/{orderId}",
  async (event) => {
    if (!event.data) return;

    const data = event.data.data();

    // Guard: skip if already processed by this function
    if (data.enrichedByFunction === true) {
      return;
    }

    // Safe to write back — the guard prevents re-triggering
    await event.data.ref.update({
      enrichedByFunction: true,
      totalWithTax: data.subtotal * 1.1,
      processedAt: admin.firestore.FieldValue.serverTimestamp(),
    });
  }
);
```

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

```
# Deploy only functions (not hosting, rules, etc.)
firebase deploy --only functions

# Deploy a specific function
firebase deploy --only functions:onOrderCreated

# View logs after triggering
firebase functions:log --only onOrderCreated
```

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

## Complete code example

File: `functions/src/index.ts`

```typescript
import * as admin from "firebase-admin";
import { logger } from "firebase-functions";
import {
  onDocumentCreated,
  onDocumentUpdated,
  onDocumentDeleted,
} from "firebase-functions/v2/firestore";
import { getFirestore } from "firebase-admin/firestore";

admin.initializeApp();
const db = getFirestore();

// Trigger: new order created
export const onOrderCreated = onDocumentCreated(
  "orders/{orderId}",
  async (event) => {
    if (!event.data) return;
    const order = event.data.data();
    const orderId = event.params.orderId;

    // Guard against infinite loop
    if (order.processedByFunction) return;

    // Enrich order with computed fields
    await event.data.ref.update({
      processedByFunction: true,
      totalWithTax: order.subtotal * 1.1,
      createdAt: admin.firestore.FieldValue.serverTimestamp(),
    });

    logger.info(`Processed new order ${orderId}`);
  }
);

// Trigger: order status changed
export const onOrderStatusChanged = onDocumentUpdated(
  "orders/{orderId}",
  async (event) => {
    if (!event.data) return;
    const before = event.data.before.data();
    const after = event.data.after.data();

    if (before.status === after.status) return;

    logger.info(
      `Order ${event.params.orderId}: ${before.status} → ${after.status}`
    );

    // Example: update a dashboard counter
    if (after.status === "completed") {
      const statsRef = db.doc("stats/orders");
      await statsRef.update({
        completedCount: admin.firestore.FieldValue.increment(1),
      });
    }
  }
);

// Trigger: user deleted — clean up related data
export const onUserDeleted = onDocumentDeleted(
  "users/{userId}",
  async (event) => {
    if (!event.data) return;
    const userId = event.params.userId;

    const postsSnap = await db
      .collection("posts")
      .where("authorId", "==", userId)
      .get();

    const batch = db.batch();
    postsSnap.docs.forEach((doc) => batch.delete(doc.ref));
    await batch.commit();

    logger.info(`Cleaned up ${postsSnap.size} posts for user ${userId}`);
  }
);
```

## Common mistakes

- **Writing back to the same document that triggered the function without a guard, causing an infinite loop** — undefined Fix: 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.
- **Using v1 trigger syntax (functions.firestore.document().onCreate) instead of v2 modular imports** — undefined Fix: Import from firebase-functions/v2/firestore and use onDocumentCreated, onDocumentUpdated, etc. The v2 API supports concurrency, longer timeouts, and Cloud Run features.
- **Not handling the case where event.data is undefined** — undefined Fix: 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.
- **Deploying functions on the free Spark plan and getting permission errors** — undefined Fix: 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

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

---

Source: https://www.rapidevelopers.com/firebase-tutorial/how-to-use-firestore-triggers-in-firebase-functions
© RapidDev — https://www.rapidevelopers.com/firebase-tutorial/how-to-use-firestore-triggers-in-firebase-functions
