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

# Firebase

> Use Cloud Functions to identify Firebase Auth users in the Sequenzy mobile SDKs and sync signups and deletions, with no backend of your own

If your app runs on Firebase, Cloud Functions can do the backend work the
Sequenzy [mobile SDKs](https://docs.sequenzy.com/concepts/mobile-sdks.md) need. This guide adds three
small functions to your Firebase project:

| Function | Trigger | What it does |
| - | - | - |
| `sequenzyIdentityToken` | Called by your app | Checks the signed-in Firebase user, adds them as a contact and returns an [identity token](https://docs.sequenzy.com/api-reference/web-tracking-keys/mint-identity.md) for the SDK |
| `sequenzyUserCreated` | Firebase Auth signup | Optional. Adds users who sign up but never open the app, for example on your website |
| `sequenzyUserDeleted` | Firebase Auth deletion | Optional. Deletes the contact when the Firebase user is deleted |

Each contact gets the Firebase UID as its `externalId`, so a user who changes
their email keeps the same contact.

You can still deploy functions after Firebase Extensions shut down on March 31, 2027. This setup also replaces the Mailchimp and Customer.io
contact-sync extensions.

## Prerequisites

* A Firebase project on the Blaze plan, which Cloud Functions require.
* `firebase-functions` 6 or later and Node.js 20 or later in your `functions`
  folder.
* A publishable key for your app, created as described in
  [Mobile SDKs](https://docs.sequenzy.com/concepts/mobile-sdks.md#1-create-an-app-key).
* A secret API key from **Settings → API Keys & SMTP** with the scopes
  `subscribers:write`, `automations:trigger` and `commerce:write`. Add
  `subscribers:delete` if you deploy the deletion function.

## 1. Store your keys

Save the secret API key in Cloud Secret Manager. It never reaches your app:

```bash theme={null}
firebase functions:secrets:set SEQUENZY_API_KEY
```

Put the publishable key in `functions/.env`. It is not a secret:

```bash functions/.env theme={null}
SEQUENZY_PUBLIC_KEY=seq_pk_...
```

## 2. Add the functions

Create `functions/src/sequenzy.ts`:

```ts functions/src/sequenzy.ts theme={null}
import { logger } from "firebase-functions";
import { defineSecret, defineString } from "firebase-functions/params";
import * as functionsV1 from "firebase-functions/v1";
import { HttpsError, onCall } from "firebase-functions/v2/https";

const SEQUENZY_API_KEY = defineSecret("SEQUENZY_API_KEY");
const SEQUENZY_PUBLIC_KEY = defineString("SEQUENZY_PUBLIC_KEY");
const SEQUENZY_API = "https://api.sequenzy.com/api/v1";

async function sequenzy(method: string, path: string, body?: unknown) {
  const response = await fetch(`${SEQUENZY_API}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${SEQUENZY_API_KEY.value()}`,
      "Content-Type": "application/json",
    },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  const data = await response.json().catch(() => ({}));
  return { ok: response.ok, status: response.status, data };
}

// Adds the user as a contact, or links an existing contact to their UID.
// "merge" only fills empty fields, so names edited in Sequenzy are kept.
async function syncContact(user: { uid: string; email: string; name?: string }) {
  const [firstName, ...rest] = (user.name ?? "").trim().split(/\s+/);
  const result = await sequenzy("POST", "/subscribers", {
    email: user.email,
    externalId: user.uid,
    ...(firstName ? { firstName } : {}),
    ...(rest.length > 0 ? { lastName: rest.join(" ") } : {}),
    duplicateStrategy: "merge",
  });
  if (!result.ok) {
    logger.warn("Sequenzy contact sync failed", { uid: user.uid, status: result.status, error: result.data });
  }
}

// Called by the app through the SDK's identity token provider.
export const sequenzyIdentityToken = onCall({ secrets: [SEQUENZY_API_KEY] }, async (request) => {
  const token = request.auth?.token;
  if (!request.auth || !token?.email) {
    throw new HttpsError("failed-precondition", "Sign in with an account that has an email address.");
  }
  // Firebase does not check email/password addresses at signup. Without this
  // check, anyone could sign up with another person's email and read their messages.
  if (!token.email_verified) {
    throw new HttpsError("permission-denied", "Verify your email address first.");
  }

  await syncContact({ uid: request.auth.uid, email: token.email, name: token.name });

  const minted = await sequenzy("POST", "/web-tracking-identities", {
    publicKey: SEQUENZY_PUBLIC_KEY.value(),
    email: token.email,
  });
  if (!minted.ok) {
    logger.error("Sequenzy identity token failed", { status: minted.status, error: minted.data });
    throw new HttpsError("unavailable", "Could not reach Sequenzy.");
  }
  return { identityToken: minted.data.identityToken as string };
});

// Optional: users who sign up without opening the app.
export const sequenzyUserCreated = functionsV1
  .runWith({ secrets: [SEQUENZY_API_KEY] })
  .auth.user()
  .onCreate(async (user) => {
    // Google, Apple and other providers verify the address. Email/password
    // users are added by sequenzyIdentityToken once they verify.
    if (!user.email || !user.emailVerified) return;
    await syncContact({ uid: user.uid, email: user.email, name: user.displayName });
  });

// Optional: permanently deletes the contact and erases their recorded data.
export const sequenzyUserDeleted = functionsV1
  .runWith({ secrets: [SEQUENZY_API_KEY] })
  .auth.user()
  .onDelete(async (user) => {
    const result = await sequenzy("DELETE", `/subscribers/external?externalId=${encodeURIComponent(user.uid)}`);
    if (!result.ok && result.status !== 404) {
      // Throwing makes the function fail; enable retries on it to try again.
      throw new Error(`Sequenzy delete failed with status ${result.status}`);
    }
  });
```

Export the functions you want from `functions/src/index.ts`, then deploy:

```ts functions/src/index.ts theme={null}
export { sequenzyIdentityToken, sequenzyUserCreated, sequenzyUserDeleted } from "./sequenzy";
```

```bash theme={null}
firebase deploy --only functions
```

The Auth triggers use the 1st-gen API (`firebase-functions/v1`). Firebase
only fires these signup and deletion events on 1st-gen functions, so the
optional functions stay 1st gen. 2nd gen only has blocking triggers, which
need Identity Platform.

## 3. Call the function from your app

Give the SDK an identity token provider that calls `sequenzyIdentityToken`,
then identify the user with their Firebase email after sign-in. The function
reads the email from the Firebase ID token and ignores anything the app sends,
so the token always belongs to the signed-in user.

> [tab] iOS
>
> Add `FirebaseFunctions` from the Firebase iOS SDK.
>
> ```swift theme={null}
> import FirebaseAuth
> import FirebaseFunctions
> import Sequenzy
>
> Sequenzy.configure(
>     publicKey: "seq_pk_...",
>     companyId: "your-company-id",
>     identityTokenProvider: { _ in
>         let result = try await Functions.functions().httpsCallable("sequenzyIdentityToken").call()
>         guard let data = result.data as? [String: Any],
>               let token = data["identityToken"] as? String else {
>             throw URLError(.badServerResponse)
>         }
>         return token
>     }
> )
>
> // After sign-in, and on sign-out:
> if let email = Auth.auth().currentUser?.email { Sequenzy.identify(email: email) }
> Sequenzy.reset()
> ```

> [tab] Android
>
> Add `com.google.firebase:firebase-functions` and
> `org.jetbrains.kotlinx:kotlinx-coroutines-play-services`.
>
> ```kotlin theme={null}
> import com.google.firebase.Firebase
> import com.google.firebase.auth.auth
> import com.google.firebase.functions.functions
> import kotlinx.coroutines.tasks.await
>
> Sequenzy.configure(
>     this,
>     publicKey = "seq_pk_...",
>     companyId = "your-company-id",
>     identityTokenProvider = { _ ->
>         val result = Firebase.functions.getHttpsCallable("sequenzyIdentityToken").call().await()
>         (result.data as Map<*, *>)["identityToken"] as String
>     },
> )
>
> // After sign-in, and on sign-out:
> Firebase.auth.currentUser?.email?.let { Sequenzy.identify(it) }
> Sequenzy.reset()
> ```

> [tab] React Native
>
> With the Firebase JS SDK:
>
> ```tsx theme={null}
> import { getAuth } from "firebase/auth";
> import { getFunctions, httpsCallable } from "firebase/functions";
> import { Sequenzy } from "@sequenzy/react-native";
>
> const getIdentityToken = httpsCallable<void, { identityToken: string }>(
>   getFunctions(),
>   "sequenzyIdentityToken"
> );
>
> Sequenzy.configure({
>   publicKey: "seq_pk_...",
>   companyId: "your-company-id",
>   appId: Application.applicationId!,
>   platform: Platform.OS === "ios" ? "ios" : "android",
>   identityTokenProvider: async () => (await getIdentityToken()).data.identityToken,
> });
>
> // After sign-in, and on sign-out:
> const email = getAuth().currentUser?.email;
> if (email) await Sequenzy.identify(email);
> await Sequenzy.reset();
> ```
>
> With React Native Firebase, call
> `functions().httpsCallable("sequenzyIdentityToken")()` instead and read
> `identityToken` from its `data`.

> [tab] Flutter
>
> Add `cloud_functions` and `firebase_auth`.
>
> ```dart theme={null}
> import 'package:cloud_functions/cloud_functions.dart';
> import 'package:firebase_auth/firebase_auth.dart';
> import 'package:sequenzy_flutter/sequenzy_flutter.dart';
>
> Sequenzy.configure(SequenzyConfig(
>   publicKey: 'seq_pk_...',
>   companyId: 'your-company-id',
>   appId: 'com.example.app',
>   platform: Platform.isIOS ? 'ios' : 'android',
>   identityTokenProvider: (email) async {
>     final result = await FirebaseFunctions.instance.httpsCallable('sequenzyIdentityToken').call();
>     return (result.data as Map)['identityToken'] as String;
>   },
> ));
>
> // After sign-in, and on sign-out:
> final email = FirebaseAuth.instance.currentUser?.email;
> if (email != null) await Sequenzy.identify(email);
> await Sequenzy.reset();
> ```

If you deploy the function outside `us-central1`, pass the same region to the
Functions client, for example `Functions.functions(region: "europe-west1")`.

Identity tokens last 24 hours, so the SDK calls the function about once a day
per device, plus once after each sign-in. Only identify users who have an
email address: anonymous and phone-only Firebase users get a
`failed-precondition` error, and the SDK retries every minute.

## What gets synced

| Sequenzy field | Firebase source |
| - | - |
| `email` | The user's email |
| `externalId` | The Firebase UID |
| `firstName` | First word of the display name, if the contact has none |
| `lastName` | Rest of the display name, if the contact has none |

Add your own fields to `syncContact` as `customAttributes`, for example a plan
or signup source. For values that change often, use `setProfile` in the app
or [Update Subscriber](https://docs.sequenzy.com/api-reference/subscribers/update.md) from another
function.

New contacts join your workspace default lists and start sequences with a
**Contact Added** trigger. The workspace double opt-in setting applies. To skip
both, add `enrollInSequences: false` or `optInMode: "confirmed"` to the
request body; see [Create Subscriber](https://docs.sequenzy.com/api-reference/subscribers/create.md).

## Existing users

The functions only add users when they next open the app or sign up. To import
users who signed up earlier, export them with
`firebase auth:export users.json --format=json` and send the ones with
`emailVerified: true` to [Create Subscriber Import](https://docs.sequenzy.com/api-reference/subscribers/import-create.md)
in batches of up to 5,000, with `localId` as `externalId`. Convert each user's
`createdAt` (milliseconds in the export) to an ISO date to keep their signup
date. Imports do not start sequences unless you pass `enrollInSequences: true`.

## Troubleshooting

* **`permission-denied` from the function**: the user's email is not verified.
  Send a verification email with Firebase Auth, then call `identify` again
  after they verify.
* **`unavailable` from the function**: Sequenzy rejected the token request.
  The function log shows the status and error. A `403` means the API key is
  missing a scope listed above, and a `400` means the email cannot be a
  contact, for example because it is suppressed.
* **`not-found` or `internal` in the app**: the function is not deployed in
  the region the client calls, or the app uses a different Firebase project.
* **Events do nothing after sign-in**: the email passed to `identify` must be
  the Firebase user's email. A token minted for one email does not work for
  another.

## FAQ

> [accordion] Do I still need a backend?
>
> No. The callable function is the backend. It holds the secret API key and
> checks the Firebase ID token, so your app never sees the key.

> [accordion] What about Sign in with Apple private relay addresses?
>
> They become contacts like any other email. Apple only forwards mail from
> domains you register in your Apple Developer account, so add your
> Sequenzy sending domain there.

> [accordion] Can I sync Firestore data?
>
> Yes. Add a Firestore trigger, such as `onDocumentWritten("users/{uid}")`,
> that sends the fields you need to
> [Update Subscriber](https://docs.sequenzy.com/api-reference/subscribers/update.md) with
> `PATCH /api/v1/subscribers/external?externalId={uid}`.

> [accordion] Can I use Firebase Analytics events?
>
> Not directly. Turn on the BigQuery export in Firebase and import the
> events with [warehouse sync](https://docs.sequenzy.com/integrations/warehouse-sync.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.