---
title: Remote Notification support
description: Display remote (APNs / FCM) notifications with full Notify Kit control via a Notification Service Extension.
---

`react-native-notify-kit` supports remote (push) notifications through Apple Push Notification service (APNs). Because iOS displays alert-style APNs pushes through the OS itself, any client-side rewriting (attachments, category, interruption level, custom sound from a bundled resource, …) must happen inside a **Notification Service Extension** (NSE) — a separate bundle that the OS runs when the push arrives. This page explains how to set one up.

> **The recommended path is [FCM Mode](/fcm-mode).** It ships an automated CLI (`npx react-native-notify-kit init-nse`) that scaffolds a Swift NSE target using `NotifeeExtensionHelper`, patches your `.pbxproj` and `Podfile`, and pairs with a server SDK (`react-native-notify-kit/server`) that builds the correct APNs payload for you. The manual Objective-C pattern documented further down is kept for apps that need custom NSE logic or have an existing integration that is not yet migrated.

## Recommended: FCM Mode

The CLI + server SDK combination is a full, tested replacement for the manual setup below:

```bash
npx react-native-notify-kit init-nse
```

This generates a `NotifyKitNSE` target with a Swift `NotificationService.swift` that calls `NotifeeExtensionHelper.populateNotificationContent` on every incoming push, plus `[NotifyKitNSE]` NSLog diagnostics visible in Console.app. On the server side:

```ts
import { buildNotifyKitPayload } from 'react-native-notify-kit/server';
import * as admin from 'firebase-admin';

const message = buildNotifyKitPayload({
  token: deviceToken,
  notification: {
    id: 'order-42',
    title: 'Your order is on the way',
    body: 'Tap to see live tracking.',
    ios: {
      categoryId: 'ORDER_UPDATE',
      sound: 'default',
      interruptionLevel: 'timeSensitive',
      attachments: [{ url: 'https://cdn.example.com/orders/42.png' }],
    },
  },
});
await admin.messaging().send(message);
```

The server SDK sets `mutable-content: 1` automatically so the NSE always fires, duplicates title/body into `aps.alert` so the OS can display a banner before the NSE finishes, and writes the `notifee_options` blob into `apns.payload`.

On the client, `notifee.handleFcmMessage(remoteMessage)` is a no-op on iOS in background/killed (the NSE already displayed the notification) and a foreground banner otherwise. See the full [FCM Mode guide](/fcm-mode) for architecture, payload schema, and troubleshooting.

## Using APNs keys only (no Notify Kit-side rewriting)

If you don't need attachment, category, sound-file, or interruption-level rewriting on the client, you can send a plain APNs payload and iOS will display it directly — no NSE required:

```json
{
  "notification": { "title": "A notification title!", "body": "A notification body" },
  "apns": {
    "payload": {
      "aps": {
        "category": "post",
        "sound": "media/kick.wav"
      }
    }
  }
}
```

This works, but you cannot use Notify Kit's iOS-specific features (attachments from arbitrary URLs, `categoryId` routing to a library-registered category, custom `interruptionLevel`, `foregroundPresentationOptions`) on the alert itself without an NSE.

## Manual NSE setup (legacy)

For apps that need custom NSE logic — or an existing integration that can't adopt the CLI — you can still wire up the NSE by hand. The steps below produce an NSE target that invokes the same `NotifeeExtensionHelper` that FCM Mode uses.

### 1. Add the Notification Service Extension target

- Xcode → **File → New → Target…**
- Select **Notification Service Extension**, click **Next**
- Product name: e.g. `NotifeeNotificationService`
- Make sure the new target's **deployment target** matches your app's (at least **iOS 15.1**, matching `RNNotifee.podspec`)

### 2. Wire the Podfile

```ruby
$NotifeeExtension = true

target 'NotifeeNotificationService' do
  pod 'RNNotifeeCore', :path => '../node_modules/react-native-notify-kit/RNNotifeeCore.podspec'
end
```

`$NotifeeExtension = true` tells the main `RNNotifee` pod to exclude `NotifeeExtensionHelper.{h,m}` so the extension target owns them (avoids the v9.1.22 duplicate-symbols linker error with `use_frameworks! :linkage => :static`).

```bash
cd ios && pod install --repo-update
```

### 3. Hook the helper into `NotificationService.m`

In the generated `NotificationService.m`:

```objectivec
#import "NotificationService.h"
#import "NotifeeExtensionHelper.h"

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request
                   withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
  self.contentHandler = contentHandler;
  self.bestAttemptContent = [request.content mutableCopy];

  [NotifeeExtensionHelper populateNotificationContent:request
                                          withContent:self.bestAttemptContent
                                   withContentHandler:contentHandler];
}

- (void)serviceExtensionTimeWillExpire {
  // Safety net: hand back whatever we have if we ran out of time.
  if (self.contentHandler && self.bestAttemptContent) {
    self.contentHandler(self.bestAttemptContent);
  }
}

@end
```

Build the app once and confirm it still compiles with the NSE target selected.

> Notify Kit's NSE helper/core path is designed to compile inside an app extension target. Current versions keep app-only APIs out of the code compiled by the extension.

> The CLI-generated Swift NSE also caps the `NSURLSession` request and resource timeouts at 25 s (per v9.4.0) to leave a ~5 s margin before iOS's 30 s NSE budget expires. If you use the manual Objective-C pattern above, consider setting the same cap on your own `NSURLSession` if you fetch large attachments.

### 4. Choose the payload

Notify Kit reads `notifee_options` from `apns.payload` (not from `aps`) and reshapes the notification before the OS displays it.

> `mutable-content: 1` (`mutableContent` in Firebase Admin SDK) is **required** on every payload that should trigger the NSE. Without it, iOS will not invoke the extension.
>
> `content-available: 1` (`contentAvailable`) is required if you also want the JS `onMessage` handler to fire while the app is in the foreground.

```json
{
  "notification": { "title": "A notification title!", "body": "A notification body" },
  "apns": {
    "payload": {
      "aps": {
        "mutableContent": 1,
        "contentAvailable": 1
      },
      "notifee_options": {
        "ios": {
          "sound": "media/kick.wav",
          "categoryId": "post",
          "attachments": [{ "url": "https://placeimg.com/640/480/any", "thumbnailHidden": true }]
        }
      }
    }
  }
}
```

`notifee_options` accepts most `Notification` and `NotificationIOS` fields. Do **not** include a client-side `foregroundPresentationOptions` here — the NSE cannot configure foreground presentation (the NSE runs only when the app is not already displaying the notification itself). If both `ios.attachments` and a top-level `image` are present, `ios.attachments` takes precedence.

Alternatively you can mutate `bestAttemptContent.userInfo[@"notifee_options"]` from inside `didReceiveNotificationRequest` before handing off to `NotifeeExtensionHelper`:

```objectivec
NSMutableDictionary *userInfoDict = [self.bestAttemptContent.userInfo mutableCopy];
NSMutableDictionary *opts = [NSMutableDictionary dictionary];
opts[@"title"] = @"Modified Title";
userInfoDict[@"notifee_options"] = opts;
self.bestAttemptContent.userInfo = userInfoDict;
```

The `id` of the displayed notification is always `request.identifier` (iOS does not let the NSE override it). Omit `id` from `notifee_options`.

## Disabling Notify Kit's remote-notification delegate

By default, the library swizzles the `UNUserNotificationCenter` delegate so tap events for remote notifications flow through Notify Kit's event system. If you are using `@react-native-firebase/messaging` (or another library) and want its original `onNotificationOpenedApp()` / `getInitialNotification()` to handle taps instead, opt out at startup:

```js
import notifee from 'react-native-notify-kit';

await notifee.setNotificationConfig({
  ios: { handleRemoteNotifications: false },
});
```

When disabled, Notify Kit no longer intercepts remote-notification tap callbacks. Local notifications, trigger notifications, and NSE enrichment continue to work unchanged — only the delegate-forwarding path is relaxed. Call this once, early in startup, before any notifications are displayed.

## Handling events

Notify Kit emits the same event types for remote notifications that it does for local ones:

- `EventType.PRESS` — user tapped the notification
- `EventType.ACTION_PRESS` — user tapped a Quick Action
- `EventType.DISMISSED` — user explicitly dismissed the notification (only fired for notifications with a `categoryId`)

Use `notification.remote` to tell whether an event came from a remote push:

```tsx
import { useEffect } from 'react';
import notifee, { EventType } from 'react-native-notify-kit';

export default function App() {
  useEffect(() => {
    return notifee.onForegroundEvent(({ type, detail }) => {
      if (detail.notification?.remote) {
        console.log('Remote notification:', detail.notification);
      }

      if (type === EventType.PRESS) {
        // ...
      } else if (type === EventType.DISMISSED) {
        // ...
      }
    });
  }, []);

  return null;
}
```

> On iOS, `DISMISSED` is only delivered for notifications that were displayed with a `categoryId`. That is a platform constraint — `UNUserNotificationCenter` only fires the dismiss callback for categorised notifications. If you need dismiss tracking on every push, ensure every payload carries a `categoryId` that matches a registered category (see [Categories & Actions](/react-native/ios/categories)).
>
> Background taps are routed to `onBackgroundEvent` (not `onForegroundEvent`) on **v9.2.1 and later** — earlier fork versions had a bug that routed them to foreground. See [Interaction](/react-native/ios/interaction#foreground-vs-background-routing).
