> ## Documentation Index
> Fetch the complete documentation index at: https://www.courier.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message` from the Node SDK (`@trycourier/courier` v7 and later, where the client is the default import). Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Send a push from Firestore

> Turn a Firestore document write into a push that falls back to email.

export const Tags = ({items}) => {
  const routes = {
    Email: "/integrations/email/overview",
    SMS: "/integrations/sms/overview",
    Push: "/integrations/push/overview",
    Inbox: "/in-app/overview",
    Chat: "/integrations/direct-message/overview",
    Templates: "/design/templates/overview",
    Variables: "/design/templates/variables",
    Elemental: "/design/elemental/overview",
    Brands: "/design/brands",
    Translations: "/design/elemental/locales",
    Routing: "/send/routing",
    Preferences: "/recipients/preferences/overview",
    Journeys: "/journeys/overview",
    Broadcasts: "/broadcasts/overview",
    Tenants: "/tenants/overview",
    Logs: "/monitor/overview",
    Webhooks: "/monitor/webhooks/outbound",
    Lists: "/recipients/lists-and-audiences/overview",
    Users: "/recipients/overview",
    Digests: "/journeys/nodes/digest",
    Environments: "/workspaces/overview",
    MCP: "/resources/mcp"
  };
  const icons = {
    Email: "envelope",
    SMS: "comment",
    Push: "mobile",
    Inbox: "inbox",
    Chat: "comments",
    Templates: "pen-ruler",
    Variables: "pen-ruler",
    Elemental: "pen-ruler",
    Brands: "pen-ruler",
    Translations: "pen-ruler",
    Routing: "paper-plane",
    Preferences: "users",
    Journeys: "route",
    Broadcasts: "bullhorn",
    Tenants: "building",
    Logs: "chart-simple",
    Webhooks: "chart-simple",
    Lists: "users",
    Users: "users",
    Digests: "route",
    Environments: "briefcase",
    MCP: "toolbox"
  };
  const base = "https://d3gk2c5xim1je2.cloudfront.net/fontawesome/v7.2.0/regular/";
  const names = String(items || "").split(",").map(entry => entry.trim()).filter(Boolean);
  return <div className="cx-tags">
      {names.map(name => {
    const href = routes[name];
    const icon = icons[name];
    const url = icon ? "url(" + base + icon + ".svg)" : null;
    const style = url ? {
      "--cx-tag-icon": url
    } : null;
    if (!href) {
      return <span className="cx-tag" data-icon={icon} style={style} key={name}>
              {name}
            </span>;
    }
    return <a className="cx-tag" data-icon={icon} style={style} href={href} key={name}>
            {name}
          </a>;
  })}
    </div>;
};

export const AppLink = ({href, children, name, bare}) => {
  const label = children || name || "Open in Courier";
  if (bare) {
    return <a href={href} target="_blank" rel="noreferrer">{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="app" href={href} target="_blank" rel="noreferrer">
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method" aria-hidden="true">↗</span>
    </a>;
};

export const Guide = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="guide" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">GUIDE</span>
    </a>;
};

export const Doc = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="doc" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">DOC</span>
    </a>;
};

<Tags items="Push, Email" />

A document lands in Firestore and someone should know. This guide sends a push, and an email instead when the push cannot land.

## What you will build

```mermaid theme={null}
flowchart LR
    A["Firestore write"] --> B["Cloud Function"]
    B --> C{"Push delivers?"}
    C -->|Yes| D["Push arrives"]
    C -->|No| E["Email instead"]
```

## Prerequisites

* <AppLink href="https://app.courier.com/~/test/platform/api-keys">A Courier Test API key</AppLink>
* <Guide href="/docs/guides/set-up-mobile-push">Push already working</Guide>, with FCM connected and device tokens syncing
* A published <Doc href="/docs/design/templates/overview">template</Doc> with push and email content

## What this adds over calling FCM yourself

You already have Firebase, so calling FCM from the same function is one line shorter. It is worth being precise about what the extra hop buys, because "use a platform" is not an argument.

**You address a person, not a device.** FCM needs a registration token. Courier needs a `user_id`, and resolves the tokens itself, including the case where someone has three devices and one stale token.

**A failed push becomes an email.** FCM can tell you a token was rejected. It cannot then reach the person another way. One `routing` line does.

**Preferences apply without a conditional.** If the recipient muted this notification type, Courier filters it. With raw FCM that check is code you write and maintain at every call site.

**One log covers both channels.** A push that failed and an email that succeeded are the same message in <AppLink href="https://app.courier.com/logs">Logs</AppLink>, not two systems to correlate.

If none of those matter for a given notification, call FCM directly. They usually start mattering around the second channel.

## Set it up

<Steps>
  <Step title="Install and store the key">
    In your functions directory:

    ```bash theme={null}
    npm install firebase-functions @trycourier/courier
    firebase functions:secrets:set COURIER_API_KEY
    ```
  </Step>

  <Step title="Write the trigger">
    This is a 2nd-gen Firestore trigger. The path carries a wildcard, and its value arrives on `event.params`.

    ```javascript functions/index.js lines theme={null}
    const { onDocumentCreated } = require("firebase-functions/v2/firestore");
    const Courier = require("@trycourier/courier");

    exports.onNewComment = onDocumentCreated(
      { document: "documents/{docId}/comments/{commentId}", secrets: ["COURIER_API_KEY"] },
      async (event) => {
        const snapshot = event.data;
        if (!snapshot) return; // deleted before the function ran

        const comment = snapshot.data();
        const { docId } = event.params;

        const courier = new Courier({ apiKey: process.env.COURIER_API_KEY });

        await courier.send.message({
          message: {
            to: { user_id: comment.recipientId },
            template: "nt_01kx4h2jdafq8bk9aftxak4b40",
            // Try push. If it cannot land, send the email instead.
            routing: { method: "single", channels: ["push", "email"] },
            data: {
              author: comment.authorName,
              excerpt: comment.text.slice(0, 140),
              document_id: docId,
            },
          },
        });
      },
    );
    ```

    Two lines are doing the work worth noticing.

    **`routing: { method: "single", channels: ["push", "email"] }`** walks the list and stops at the first channel that works. Push first, email as its failover. Swap to `all` and both go out every time.

    **`if (!snapshot) return`** guards the case where the document is gone by the time the function runs. Firestore triggers are not transactional with the write.
  </Step>

  <Step title="Deploy">
    ```bash theme={null}
    firebase deploy --only functions:onNewComment
    ```
  </Step>
</Steps>

## Verify

<Steps>
  <Step title="Write a document">
    Add a comment document under a path matching the trigger, from your app or the Firebase console.
  </Step>

  <Step title="Check the function ran">
    ```bash theme={null}
    firebase functions:log --only onNewComment
    ```
  </Step>

  <Step title="Confirm which channel delivered">
    Open <AppLink href="https://app.courier.com/logs">Logs</AppLink>. The timeline names the channel that delivered. To prove the fallback, remove the user's device tokens and write again. The same send should arrive by email.
  </Step>
</Steps>

## Adapt it for other writes

| Trigger             | What to send                                                                                    |
| :------------------ | :---------------------------------------------------------------------------------------------- |
| `onDocumentCreated` | The new thing happened. A comment, an order, an invite                                          |
| `onDocumentUpdated` | Compare `event.data.before` to `event.data.after` and send only when something meaningful moved |
| `onDocumentDeleted` | A cancellation or removal confirmation                                                          |

The update case is the one to be careful with. A trigger on every write fires on fields nobody cares about, so compare before and after and return early. That is the difference between a useful alert and a reason to mute your app.

For fan-out, when many people are watching the document rather than one, address a list instead of a user. <Guide href="/docs/guides/notify-everyone-watching">Notify everyone watching</Guide> covers that shape.
