React Native push notifications with FCM and APNs: a practical guide
Firebase project, APNs .p8 key, permissions on Android 13+ and iOS, foreground vs background handling, and opening the right screen from a notification, with code.
Push notifications in React Native touch more moving parts than most features: a Firebase project, an Apple key, two permission models, three app states, and a payload format for each platform. This guide walks through them in the order you'll meet them, with the code that matters at each step.
It uses @react-native-firebase/messaging, the most common way to receive Firebase Cloud Messaging (FCM) in React Native, and the modular API its current docs use. If you'd rather not run the sending side yourself, the end of the post covers that too.
How push works on Android and iOS
Your server never talks to a phone directly. It sends a message to a platform service, and that service delivers it:
- Android: your server → FCM → the device.
- iOS: your server → APNs (Apple Push Notification service) → the device. If you send through Firebase, FCM forwards iOS messages to APNs for you, using an APNs key you upload to Firebase.
Each install gets a device token from the platform. Your app sends it to your backend, and your backend sends to that token. No token, no push.
What you need before you start
- A Firebase project with your Android app (package name) and iOS app (bundle ID) registered, and their
google-services.jsonandGoogleService-Info.plistfiles. - An Apple Developer account to create an APNs key.
- A physical iPhone. React Native Firebase's docs note that APNs registration doesn't complete on Apple Silicon simulators, so test delivery end to end on a real device.
- An Android device or emulator with Google Play services.
Step 1: Install React Native Firebase messaging
npm install @react-native-firebase/app @react-native-firebase/messaging
cd ios && pod install
Add google-services.json to android/app/ and GoogleService-Info.plist to your iOS target, then follow the React Native Firebase installation guide for the native lines on each platform. Rebuild the app afterwards. New native modules don't load through a JavaScript reload.
Step 2: Create an APNs key and turn on capabilities
In Apple's developer portal, go to Certificates, Identifiers & Profiles → Keys, create a key with Apple Push Notifications service (APNs) enabled, and download the .p8 file. Apple only lets you download it once, so store it safely. Note the Key ID and your Team ID.
Apple now lets you limit a key to the Sandbox or Production environment (token-based connection docs). Builds you run from Xcode use the sandbox; TestFlight and App Store builds use production. Make sure the key you use covers the builds you're testing.
If you send through Firebase, upload the key in the Firebase console under Project settings → Cloud Messaging, in the iOS app configuration section, with its Key ID.
In Xcode, under your target's Signing & Capabilities, add:
- Push Notifications
- Background Modes, with Remote notifications checked (and Background fetch, as React Native Firebase's iOS guide suggests)
Step 3: Ask for permission on iOS and Android 13+
The two platforms ask differently:
- iOS has always required permission. The system dialog appears once; if the user declines, only the Settings app can turn notifications back on. iOS 12 and later also support provisional permission, which delivers quietly to Notification Center without asking first.
- Android 13 (API 33) and later require the runtime permission
POST_NOTIFICATIONS. Android 12 and earlier allow notifications by default.
Declare the Android permission in AndroidManifest.xml:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
React Native Firebase's own permission APIs are deprecated, and its docs point to react-native-permissions or expo-notifications instead. With react-native-permissions (add 'Notifications' to setup_permissions in your Podfile):
import { requestNotifications } from "react-native-permissions";
export async function askForPush() {
const { status } = await requestNotifications(["alert", "sound", "badge"]);
return status === "granted";
}
The options array only applies on iOS. On Android 13+, your app must target SDK 33 or higher for the runtime request to appear.
Ask at a moment that explains itself. "Get notified when your order ships", shown after checkout, gets a better answer than a dialog on first launch. Since you only get one system dialog on iOS, many apps show their own explainer screen first and only call requestNotifications when the user taps Continue.
Step 4: Get the device token and send it to your server
import { getMessaging, getToken, onTokenRefresh } from "@react-native-firebase/messaging";
const messaging = getMessaging();
export async function registerForPush(userId: string) {
const token = await getToken(messaging);
await saveToken(userId, token); // your API call
return onTokenRefresh(messaging, (next) => saveToken(userId, next));
}
Tokens change: after a reinstall, when the app's data is cleared, or when the platform rotates them. Save the newest one, and delete tokens your sender reports as invalid so your device counts stay honest. React Native Firebase's docs note that newer token APIs are on the way upstream; check them when you upgrade.
Step 5: Handle foreground, background and quit states
What happens when a message arrives depends on the app's state:
| App state | Notification message (has a title and body) | What your code does |
|---|---|---|
| Foreground | Not shown by the system | onMessage runs. Show your own banner, or a local notification with a library such as Notifee. |
| Background | Shown in the system tray | setBackgroundMessageHandler runs, if registered |
| Quit | Shown in the system tray | setBackgroundMessageHandler runs, if registered |
The foreground row surprises people: React Native Firebase's docs say that when a notification message arrives while the app is open, "the device will not show any notification to the user". You decide what to show.
// App.tsx
import { useEffect } from "react";
import { getMessaging, onMessage } from "@react-native-firebase/messaging";
useEffect(
() =>
onMessage(getMessaging(), async (message) => {
showInAppBanner(message.notification?.title, message.notification?.body);
}),
[],
);
Register the background handler in index.js, before AppRegistry, so it exists even when the app starts headless:
// index.js
import { AppRegistry } from "react-native";
import { getMessaging, setBackgroundMessageHandler } from "@react-native-firebase/messaging";
import App from "./App";
setBackgroundMessageHandler(getMessaging(), async (message) => {
// Update local storage or counters. Don't touch UI state here.
});
AppRegistry.registerComponent("app", () => App);
Data-only messages (a data payload with no notification) are treated as low priority in the background and may not be delivered right away. To wake your handler, send them with priority: "high" on Android and content-available: 1 on iOS, with the apns-push-type header set to background.
Step 6: Open the right screen when a notification is tapped
Put the destination in the message's data, usually as a URL:
{ "data": { "url": "https://links.example.com/orders/42" } }
Then handle the tap in both of the states where it can happen:
import { useEffect } from "react";
import { Linking } from "react-native";
import { getMessaging, getInitialNotification, onNotificationOpenedApp } from "@react-native-firebase/messaging";
useEffect(() => {
const messaging = getMessaging();
// The app was quit and the tap launched it.
getInitialNotification(messaging).then((message) => {
const url = message?.data?.url;
if (typeof url === "string") Linking.openURL(url);
});
// The app was in the background.
return onNotificationOpenedApp(messaging, (message) => {
const url = message.data?.url;
if (typeof url === "string") Linking.openURL(url);
});
}, []);
Using a link instead of a screen name means one routing path for everything: the same URL works in a notification, an email, a QR code and a WhatsApp message. If it's a Universal Link or App Link, your existing deep link handling opens the screen. Our React Native deep linking guide covers that side.
One caveat from React Native Firebase's docs: if you use @notifee/react-native 7.0.0 or later to display notifications, getInitialNotification and onNotificationOpenedApp no longer fire for them. Handle taps with Notifee's events instead.
Step 7: Send a test message
From a Node.js backend, the Firebase Admin SDK handles authentication with your service account:
import { initializeApp, cert } from "firebase-admin/app";
import { getMessaging } from "firebase-admin/messaging";
import serviceAccount from "./service-account.json" with { type: "json" };
initializeApp({ credential: cert(serviceAccount) });
await getMessaging().send({
token: deviceToken,
notification: { title: "Your order has shipped", body: "Arriving Thursday" },
data: { url: "https://links.example.com/orders/42" },
android: { priority: "high" },
apns: { payload: { aps: { sound: "default" } } },
});
When notifications don't arrive
- iOS Simulator. Test on a real device.
- Wrong APNs environment. A sandbox token sent to production (or the reverse) is rejected. Check which build you installed and which environment your key allows.
- Permission off. Check iOS Settings → your app → Notifications, and on Android 13+ whether the runtime permission was granted.
- Stale token. Log the token on launch and compare it with what your server has.
- App in the foreground. It arrived; nothing showed because that's your code's job (Step 5).
- Android battery settings. Some manufacturers restrict background apps aggressively. Test on the devices your users have.
Sending without running it yourself: CodeOnAir Push
Receiving is the app's job. Sending at scale is a backend job: storing tokens, targeting users, scheduling, retrying, and cleaning up dead tokens. CodeOnAir Push does the sending side. You upload your Firebase service account JSON for Android and your APNs .p8 key for iOS, and they are checked with Google and Apple when you save them. Then you send from the dashboard, or from your backend:
curl https://app.codeonair.com/v1/push/messages \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"title": "Your order has shipped",
"body": "Arriving Thursday",
"url": "https://shop.codeonair.com/orders/42",
"audience": { "user_ids": ["user_1024"] }
}'
Audiences can be every device, one platform, signed-in user IDs, or tags. You can send now or schedule up to 90 days ahead, add an image and a link that opens on tap, and see sent, failed and opened counts. Put an AppLink in the url, and the tap opens the right screen even for links shared outside the app.
What isn't ready yet: our React Native push SDK, which registers devices with CodeOnAir and shows in-app messages, hasn't been released. It's coming. The push setup docs cover credentials today. Push limits per plan are on the pricing page: 10,000 notifications a month on Free, 100,000 on Starter, 1,000,000 on Growth and 5,000,000 on Scale.
FAQ
Do I need Firebase for iOS push in React Native?
No. iOS push always goes through APNs. Firebase is one way to reach APNs with a single API for both platforms. You can also send to APNs directly with a .p8 key.
Why don't notifications show when my app is open?
By design. In the foreground, onMessage receives the message and the system doesn't display it. Show your own banner or a local notification.
How do I request notification permission on Android 13?
Declare android.permission.POST_NOTIFICATIONS in the manifest, target SDK 33 or higher, and request it at runtime, for example with requestNotifications from react-native-permissions.
Can a push notification open a specific screen?
Yes. Send a URL in the message's data, read it in getInitialNotification and onNotificationOpenedApp, and open it with your deep link handling.
Should I use a .p8 key or a .p12 certificate for APNs?
A .p8 key, in most cases. Apple's docs say provider certificates are valid for a year and each one covers a single app, while a team-scoped key can send to all of your team's apps.
Send your first push
CodeOnAir Push is included on every plan, with 10,000 notifications a month on Free. You can connect your Firebase and APNs keys in the dashboard now; device registration from React Native arrives with the push SDK. Sign-ups are currently by invitation: request access and tell us you're interested in push.
Checked on 25 October 2026 against React Native Firebase's messaging docs, Apple's APNs documentation and the react-native-permissions README.