# Push notifications

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

Push notifications tell users about new messages while the app is in the background or closed. Stream sends them through a push provider, such as Firebase Cloud Messaging, to the devices the app registered for the user. This page covers what users get, how the SDK takes part, and how to set up Firebase, register devices, show notifications and open a channel from one.

The SDK doesn't display notifications or handle taps on them. Use [React Native Firebase](https://rnfirebase.io/) to receive them and [Notifee](https://notifee.app/) to display them and to handle taps, as the example apps do.

## What users get

The user puts their phone away with the app open on the channel list:

1. The app goes to the background, and `Chat` closes its WebSocket connection.
2. Bob writes to the user in a channel they're both members of. Stream sends a push to the user's registered devices.
3. Their phone shows "New message from Bob" with the message text.
4. The user taps it. The app opens on that channel, with Bob's message in it.

Only channel members receive a push, not users who only watch the channel. Which events send one, and to whom, is decided on the server by the [push delivery rules](https://getstream.io/docs/platform/push-notifications/#push-delivery-rules).

## How it works across the layers

- **State.** A device is a push token registered for the connected user with `client.createDevice()`. `client.listDevices()` lists the user's devices, and `client.deleteDevice()` removes one. Stream sends a push to every registered device of a user, up to 25.
- **UI.** `Chat` closes the socket when the app goes to the background, and opens it again when it comes back, unless `closeConnectionOnBackground` is `false`. Every return to the foreground is then a reconnect, and the client reloads what's on screen, as [Connection](https://getstream.io/chat/docs/sdk/react-native/v10/state/connection/#closing-the-socket-in-the-background) explains. Nothing else in the SDK takes part.
- **Navigation.** A tap on a notification opens the app. Your tap handler reads the channel from the notification's data and navigates to it. [Moving between channels, threads and messages](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/moving-between-channels-threads-and-messages/#push-notifications) shows the navigation part.

Whether a user with an open socket receives pushes depends on the app. Apps created before 2022-01-18 send pushes only to users without an active WebSocket connection, and later apps send them regardless. With the socket closed in the background, both kinds of app send them while the app is in the background. See [User offline/online transition](https://getstream.io/docs/platform/push-troubleshooting/#user-offlineonline-transition).

## Setting up Firebase

1. [Create a Firebase project](https://console.firebase.google.com/), and register your [Android](https://firebase.google.com/docs/android/setup#register-app) and [iOS](https://firebase.google.com/docs/ios/setup#register-app) apps with it.
2. Upload your [APNs authentication key](https://firebase.google.com/docs/cloud-messaging/ios/client#upload_your_apns_authentication_key) to the Firebase project.
3. Install React Native Firebase and its messaging module, and set up the [Android](https://rnfirebase.io/#2-android-setup) and [iOS](https://rnfirebase.io/#3-ios-setup) apps:

   ```bash
   yarn add @react-native-firebase/app @react-native-firebase/messaging @notifee/react-native
   ```

4. Add the [Push Notifications capability](https://developer.apple.com/documentation/xcode/adding-capabilities-to-your-app) to the iOS app.

React Native Firebase needs static frameworks on iOS. With offline support, `@op-engineering/op-sqlite` then needs the fix in [op-sqlite doesn't build with static frameworks](https://getstream.io/chat/docs/sdk/react-native/v10/troubleshooting/#op-sqlite-doesnt-build-with-static-frameworks).

### Expo

React Native Firebase doesn't run in Expo Go. Use a [development build](https://docs.expo.dev/develop/development-builds/introduction/) and the config plugins of `@react-native-firebase/app` and `@react-native-firebase/messaging`, as the [React Native Firebase Expo guide](https://rnfirebase.io/#expo) explains. The `ExpoMessaging` example app has a working configuration in its `app.json`. [React Native Firebase doesn't build on Expo 54](https://getstream.io/chat/docs/sdk/react-native/v10/troubleshooting/#react-native-firebase-doesnt-build-on-expo-54) and [Notifee doesn't build in an Expo app](https://getstream.io/chat/docs/sdk/react-native/v10/troubleshooting/#notifee-doesnt-build-in-an-expo-app) cover the build errors.

### Giving Stream your Firebase credentials

Stream calls Firebase with a Google service account:

1. In the Firebase console, open **Settings > Service Accounts**.
2. Click **Generate New Private Key**, then **Generate Key**, and keep the JSON file it downloads.
3. In the [Stream Dashboard](https://getstream.io/signin/?product=chat), open your app's push notification settings, add a Firebase configuration, and upload the JSON file. Give the configuration a name. The app passes it as `push_provider_name` when it registers a device.

[Push providers and multi-bundle](https://getstream.io/docs/platform/push-providers/) explains providers, and how one Stream app serves several apps.

## Registering the device

Register the device after the user is connected. The request uses the user's token, and registers the device for that user. This hook asks for permission, registers the Firebase token, and registers it again when Firebase replaces it:

```ts title="usePushNotifications.ts"
import { useEffect } from "react";
import { PermissionsAndroid, Platform } from "react-native";
import {
  AuthorizationStatus,
  getMessaging,
  getToken,
  onTokenRefresh,
  requestPermission,
} from "@react-native-firebase/messaging";
import type { StreamChat } from "stream-chat";

const messaging = getMessaging();

// The name of the Firebase configuration in the Stream Dashboard.
const PUSH_PROVIDER_NAME = "my-app-firebase";

const requestNotificationPermission = async () => {
  if (Platform.OS === "android" && Platform.Version >= 33) {
    const result = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
    );
    return result === PermissionsAndroid.RESULTS.GRANTED;
  }
  const status = await requestPermission(messaging);
  return (
    status === AuthorizationStatus.AUTHORIZED ||
    status === AuthorizationStatus.PROVISIONAL
  );
};

// `client` is the connected client, such as the one `useCreateChatClient` returns.
export const usePushNotifications = (client: StreamChat | null) => {
  useEffect(() => {
    if (!client) return;
    let stopped = false;
    let unsubscribeTokenRefresh: (() => void) | undefined;

    const register = async () => {
      if (!(await requestNotificationPermission()) || stopped) return;

      let currentToken = await getToken(messaging);
      await client.createDevice({
        id: currentToken,
        push_provider: "firebase",
        push_provider_name: PUSH_PROVIDER_NAME,
      });
      if (stopped) return;

      unsubscribeTokenRefresh = onTokenRefresh(messaging, async (newToken) => {
        await client.deleteDevice({ id: currentToken });
        currentToken = newToken;
        await client.createDevice({
          id: newToken,
          push_provider: "firebase",
          push_provider_name: PUSH_PROVIDER_NAME,
        });
      });
    };

    void register();

    return () => {
      stopped = true;
      unsubscribeTokenRefresh?.();
    };
  }, [client]);
};
```

`client.createDevice()` takes the token as `id`, the provider (`firebase`, `apn`, `huawei` or `xiaomi`), and the name of the provider's configuration. A user can have 25 devices. When a user has 25, registering one more removes an invalid or the oldest one.

To register an APNs token directly, without Firebase, pass the token from `getAPNSToken(messaging)` with `push_provider: "apn"` and the name of an APN configuration.

### Removing the device on logout

A device stays registered after the user disconnects, so it keeps receiving that user's pushes. Remove it before you disconnect the user:

```ts title="logout.ts"
import { getMessaging, getToken } from "@react-native-firebase/messaging";
import type { StreamChat } from "stream-chat";

export const logout = async (client: StreamChat) => {
  const token = await getToken(getMessaging());
  await client.deleteDevice({ id: token });
  await client.disconnectUser();
};
```

`client.listDevices()` returns `{ devices }`, every device registered for the user, with its `id`, `push_provider` and `push_provider_name`.

## What a notification carries

The push is built from the push template of the provider's configuration. With the default Firebase template:

- iOS gets a notification with a title, "New message from Bob", the message text as its body, and the user's unread count as the app badge. The system shows it while the app is in the background or closed.
- Android gets a data message, which the system doesn't show. Your app displays it, as below.
- Both get the same data, as strings:

```json
{
  "sender": "stream.chat",
  "type": "message.new",
  "version": "v2",
  "id": "d152f6c1-8c8c-476d-bfd6-59c15c20548a",
  "message_id": "d152f6c1-8c8c-476d-bfd6-59c15c20548a",
  "channel_type": "messaging",
  "channel_id": "company-chat",
  "cid": "messaging:company-chat",
  "receiver_id": "ann"
}
```

The data has no text. To show the sender and the text on Android, add `title` and `body` to the data of the Firebase template in the Dashboard. A template that sends iOS a data-only push, so that the app displays it, puts `title` and `body` in a `stream` object instead. [Push templates](https://getstream.io/chat/docs/node/push-template/) explains the templates and their variables. They're changed in the Dashboard, or from your server with a server-side SDK.

### Customizing the template

A Firebase template has three parts: `data`, which both platforms get, `android`, and `apns`, the payload for iOS. The template is rendered with Handlebars, so `{{ sender.name }}` inserts the sender's name and `{{ truncate message.text 150 }}` the start of the text. Keep `version` set to `"v2"` in `data`. The samples below show one part each; keep the other parts as they are in the default template. [Push templates](https://getstream.io/chat/docs/node/push-template/) lists the variables and helpers.

A `notification` in the `android` part lets Android show the notification itself while the app is in the background or closed, as iOS does:

```json
{
  "android": {
    "priority": "high",
    "notification": {
      "title": "{{ sender.name }} @ {{ channel.name }}",
      "body": "{{ truncate message.text 150 }}",
      "click_action": "OPEN_ACTIVITY_1",
      "sound": "default"
    }
  }
}
```

A tap on it then arrives through `onNotificationOpenedApp` and `getInitialNotification`, as for any notification the system shows. The background handler below still runs for these messages, so return early from it when `remoteMessage.notification` is set, or the user gets the notification twice. Without `click_action`, a tap opens the app's launcher activity. With it, the tap opens the activity whose intent filter has that action, so add one to `MainActivity` in `AndroidManifest.xml`:

```xml
<intent-filter>
  <action android:name="OPEN_ACTIVITY_1" />
  <category android:name="android.intent.category.DEFAULT" />
</intent-filter>
```

The default template sets the iOS app badge to the user's unread message count. To count unread channels instead, set `badge` in the `aps` object of the `apns` part:

```json
{
  "apns": {
    "payload": {
      "aps": {
        "alert": {
          "title": "New message from {{ sender.name }}",
          "body": "{{ truncate message.text 150 }}"
        },
        "badge": {{ unread_channels }},
        "sound": "default",
        "mutable-content": 1
      }
    }
  }
}
```

To display iOS notifications with Notifee, as on Android, send iOS a data-only push. Mark it as a background push with `content-available`, and move the title and the body into a `stream` object, which `getNotificationContent` below merges into the data:

```json
{
  "apns": {
    "headers": {
      "apns-push-type": "background",
      "apns-priority": "5"
    },
    "payload": {
      "aps": {
        "content-available": 1
      },
      "stream": {
        "title": "New message from {{ sender.name }}",
        "body": "{{ truncate message.text 150 }}"
      }
    }
  }
}
```

A data-only push must stay under 4 KB, so put only what the app needs in it.

## Showing notifications in the background

On Android, display each data message yourself. Register the handler at the top of your entry file, before `AppRegistry.registerComponent`, so it runs when the app is in the background or closed:

```ts title="backgroundMessageHandler.ts"
import notifee from "@notifee/react-native";
import {
  getMessaging,
  setBackgroundMessageHandler,
} from "@react-native-firebase/messaging";
import type { FirebaseMessagingTypes } from "@react-native-firebase/messaging";

// Merges a `stream` object, which a data-only iOS template sends, into the data.
export const getNotificationContent = (
  remoteMessage: FirebaseMessagingTypes.RemoteMessage,
) => {
  const { stream, ...rest } = remoteMessage.data ?? {};
  const data = {
    ...rest,
    ...(typeof stream === "object" ? stream : {}),
  } as Record<string, string>;
  const title = data.title ?? remoteMessage.notification?.title;
  const body = data.body ?? remoteMessage.notification?.body;
  return { body, data, title };
};

setBackgroundMessageHandler(getMessaging(), async (remoteMessage) => {
  const { body, data, title } = getNotificationContent(remoteMessage);
  if (!title || !body) return;

  const channelId = await notifee.createChannel({
    id: "chat-messages",
    name: "Chat messages",
  });
  await notifee.displayNotification({
    android: { channelId, pressAction: { id: "default" } },
    body,
    data,
    title,
  });
});
```

```js title="index.js"
import "./backgroundMessageHandler";
// ...then AppRegistry.registerComponent as usual.
```

The handler runs without your app's screens, and while the socket is closed. Don't open channels or call the API from it unless you connect a client there yourself.

On iOS, the system shows the notification. `setBackgroundMessageHandler` runs on iOS only for a data-only push, which needs a template with `content-available` and the Background Modes capability with Remote notifications; React Native Firebase's [background messages](https://rnfirebase.io/messaging/usage#background--quit-state-messages) guide covers it. iOS treats such pushes as low priority and can delay or drop them.

The default template sets the app badge on iOS to the user's unread count. Clear it when the app opens, for example with `notifee.setBadgeCount(0)`.

### In the foreground

Neither platform shows a push while the app is in the foreground. Most chat apps don't need to, since the app already shows the message. To show one, listen with `onMessage(getMessaging(), handler)` and display it with Notifee, as in the background handler.

## Opening a channel from a notification

Where a tap arrives depends on what displayed the notification and on the app's state:

| Displayed by         | App closed                          | App in the background       | App in the foreground       |
| -------------------- | ----------------------------------- | --------------------------- | --------------------------- |
| The system, from FCM | `getInitialNotification(messaging)` | `onNotificationOpenedApp`   |                             |
| Notifee              | `notifee.getInitialNotification()`  | `notifee.onBackgroundEvent` | `notifee.onForegroundEvent` |

Each one gives you the notification's data, with the channel's `cid`:

```ts title="notificationTaps.ts"
import notifee, { EventType } from "@notifee/react-native";
import {
  getInitialNotification,
  getMessaging,
  onNotificationOpenedApp,
} from "@react-native-firebase/messaging";

type NotificationData = Record<string, unknown> | undefined;

const getCid = (data: NotificationData) =>
  typeof data?.cid === "string" ? data.cid : undefined;

// Call once, when the navigator is ready. Returns a function that stops listening.
export const listenForNotificationTaps = (
  openChannel: (cid: string) => void,
) => {
  const messaging = getMessaging();
  const open = (data: NotificationData) => {
    const cid = getCid(data);
    if (cid) openChannel(cid);
  };

  // A tap that opened the app from closed.
  void getInitialNotification(messaging).then((message) => open(message?.data));
  void notifee
    .getInitialNotification()
    .then((initial) => open(initial?.notification.data));

  // A tap while the app runs.
  const unsubscribeOpened = onNotificationOpenedApp(messaging, (message) =>
    open(message.data),
  );
  const unsubscribeForeground = notifee.onForegroundEvent(
    ({ detail, type }) => {
      if (type === EventType.PRESS) open(detail.notification?.data);
    },
  );

  return () => {
    unsubscribeOpened();
    unsubscribeForeground();
  };
};
```

`notifee.onBackgroundEvent` has to be registered in the entry file, next to the background message handler, and runs without your app's screens. Keep the `cid` there, and open the channel when the app's navigator is ready.

`openChannel` navigates to the channel screen. The screen gets the channel with `client.channel(type, id)`, which also works for a channel no list has loaded, and passes it to `Channel`, which watches it. A tap that opens the app arrives before the navigator is ready, so keep the `cid` and navigate once it is. [Opening from a push notification or a deep link](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/moving-between-channels-threads-and-messages/#opening-from-a-push-notification-or-a-deep-link) shows the channel screen and the navigation, with React Navigation and with Expo Router.

To open at the message, pass the data's `id` too, and jump to it once the channel screen is open.

## Troubleshooting

- Check the **Webhook & Push Logs** in the Dashboard, to see whether Stream sent the push. [Testing and troubleshooting push](https://getstream.io/docs/platform/push-troubleshooting/) covers the cases in which it doesn't.
- Check that the user is a member of the channel, has a registered device, and that the `push_provider_name` matches a configuration in the Dashboard.
- Android can restrict apps in the background to save battery, which stops notifications from showing. See Notifee's [background restrictions](https://notifee.app/react-native/docs/android/background-restrictions).
- If Stream sent the push and the device didn't show it, follow Firebase's diagnostics for [Android](https://firebase.google.com/support/troubleshooter/fcm/delivery/diagnose/android) and [iOS](https://firebase.google.com/support/troubleshooter/fcm/delivery/diagnose/ios).

## Reference

- [Chat](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/chat/chat/): the `closeConnectionOnBackground` prop.
- [Connection](https://getstream.io/chat/docs/sdk/react-native/v10/state/connection/): what closing the socket in the background does, and the reconnect after it.
- [Moving between channels, threads and messages](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/moving-between-channels-threads-and-messages/): opening a channel, a thread or a message from a notification.
- [Push notifications](https://getstream.io/docs/platform/push-notifications/): providers, devices, preferences and delivery rules.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/features/app-experience/push-notifications/](https://getstream.io/chat/docs/sdk/react-native/v10/features/app-experience/push-notifications/).