React Native Push Notifications: Complete Setup Guide (2026)
Step-by-step guide to adding push notifications to a React Native app using Firebase Cloud Messaging. Covers Android and iOS setup, permission handling, background messages, deep linking, and connecting to PushPilot.

Adding push notifications to a React Native app involves more moving parts than most tutorials let on. You're dealing with two platforms (Android and iOS) with very different permission models, a Firebase project that needs to be wired to both, background handling that runs outside your React component tree, and device token management that needs to stay in sync with your backend.
This guide covers all of it. By the end, you'll have foreground, background, and quit-state notifications working on both platforms, with a clean integration path to a push notification platform like PushPilot.
Prerequisites
Before you start, make sure you have:
Required
- ✓ React Native 0.71+ (bare workflow — not Expo Go)
- ✓ A Google Firebase project (free)
- ✓ Android: Android Studio, API level 21+ target
- ✓ iOS: Xcode 15+, Apple Developer account (paid — required for APNs)
- ✓ A physical device for final testing (simulators don't receive FCM)
Expo users
If you're using Expo managed workflow, use expo-notifications instead of @react-native-firebase/messaging. This guide is for bare React Native or Expo bare workflow only.
Get your google-services.json and GoogleService-Info.plist ready
google-services.json for Android and GoogleService-Info.plist for iOS. You'll need both in the next steps.Install @react-native-firebase
Install the core Firebase app package and the messaging module:
# Using npm
npm install @react-native-firebase/app @react-native-firebase/messaging
# Using yarn
yarn add @react-native-firebase/app @react-native-firebase/messagingThen install iOS pods:
cd ios && pod install && cd ..React Native 0.71+ uses the new architecture autolinking, so you don't need to run react-native link. If you're on an older version, check the rnfirebase.io docs for your specific version.
Android Setup
Step 1: Place google-services.json into android/app/google-services.json.
Step 2: Apply the Google Services plugin in your project-level build file:
// android/build.gradle (project-level)
buildscript {
dependencies {
// ... existing deps
classpath 'com.google.gms:google-services:4.4.1'
}
}Step 3: Apply the plugin and add Firebase dependencies in your app-level build file:
// android/app/build.gradle
apply plugin: 'com.google.gms.google-services' // add at bottom of file
dependencies {
// ... existing deps
implementation platform('com.google.firebase:firebase-bom:32.7.0')
implementation 'com.google.firebase:firebase-messaging'
}Step 4: Add the notification permission and a default channel ID to your manifest. Android 13+ (API 33) requires POST_NOTIFICATIONS at runtime — the manifest entry enables the runtime prompt:
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest ...>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application ...>
<!-- Required for heads-up notifications on Android 8+ -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="pushpilot_default"/>
</application>
</manifest>Android 13+ runtime permission
messaging().requestPermission() (covered in the next section) — the manifest entry alone is not enough.iOS Setup
Step 1: Drag GoogleService-Info.plist into your Xcode project under the YourApp folder. Make sure "Copy items if needed" is checked and the file is added to your app target.
Step 2: In Xcode → Signing & Capabilities → add the Push Notifications capability and the Background Modes capability. Under Background Modes, enable Remote notifications.
Step 3: Create or update the entitlements file:
<!-- ios/YourApp/YourApp.entitlements -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string> <!-- change to 'production' for release builds -->
</dict>
</plist>Step 4: Upload your APNs key to Firebase. In Firebase Console → Project Settings → Cloud Messaging → Apple app configuration, upload the .p8 key from your Apple Developer account (Certificates, Identifiers & Profiles → Keys → create a key with Apple Push Notifications service enabled).
APNs requires a paid Apple Developer account
Request Notification Permission
Permissions work differently per platform. iOS has always required an explicit opt-in. Android added a runtime permission requirement in Android 13 (API 33). The @react-native-firebase/messaging API handles both uniformly:
import messaging from '@react-native-firebase/messaging';
export async function requestNotificationPermission() {
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) {
console.log('Notification permission granted, status:', authStatus);
return true;
}
console.log('Notification permission denied');
return false;
}Call requestNotificationPermission() at an appropriate moment in your onboarding flow — not immediately on app launch. Showing the permission prompt in context (e.g., "Enable notifications to get order updates") dramatically increases opt-in rates compared to prompting at first open.
iOS provisional authorisation
messaging.AuthorizationStatus.PROVISIONAL means the user hasn't explicitly opted in yet, but iOS will silently deliver your notifications to the notification centre (not as banners). This is a useful foot-in-the-door — send your first few notifications this way before asking for full permission.
Get and Store the FCM Token
The FCM token is a unique identifier for a specific app installation on a specific device. You need to send this to your backend (or directly to PushPilot) so you can target that device:
import messaging from '@react-native-firebase/messaging';
export async function getFCMToken(): Promise<string | null> {
try {
// Check if device supports FCM (physical device only — not simulator)
if (!messaging().isDeviceRegisteredForRemoteMessages) {
await messaging().registerDeviceForRemoteMessages();
}
const token = await messaging().getToken();
console.log('FCM Token:', token);
return token;
} catch (error) {
console.error('Failed to get FCM token:', error);
return null;
}
}
// Listen for token refreshes — always update your backend
messaging().onTokenRefresh(async (newToken) => {
console.log('FCM token refreshed:', newToken);
await sendTokenToBackend(newToken);
});Once you have the token, send it to your backend or push notification platform:
async function sendTokenToBackend(token: string) {
await fetch('https://api.yourapp.com/devices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fcm_token: token,
platform: Platform.OS, // 'ios' or 'android'
user_id: getCurrentUserId(), // your auth user id
}),
});
}Always handle token refreshes
onTokenRefresh, you'll eventually be sending notifications to stale tokens, which degrades deliverability.Handle Foreground, Background, and Quit-State Notifications
React Native apps have three lifecycle states for incoming notifications. Each requires a different handler.
Foreground and background (inside component tree)
import messaging, { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
import { useEffect } from 'react';
export function useNotificationListeners() {
useEffect(() => {
// 1. Foreground messages (app is open and in focus)
const unsubscribeForeground = messaging().onMessage(
async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
console.log('Foreground notification received:', remoteMessage);
// Show an in-app banner or update UI state
showInAppNotification(remoteMessage.notification);
}
);
// 2. User tapped a notification while app was in background
const unsubscribeBackground = messaging().onNotificationOpenedApp(
(remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
console.log('Notification tapped (background):', remoteMessage);
handleNotificationNavigation(remoteMessage.data);
}
);
return () => {
unsubscribeForeground();
unsubscribeBackground();
};
}, []);
}Background and quit-state handler (outside component tree)
This handler runs in a separate JavaScript context. It must be registered in your entry file before AppRegistry.registerComponent:
// index.js (or your app entry point — OUTSIDE the component tree)
import { AppRegistry } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import App from './App';
import { name as appName } from './app.json';
// Background / quit-state handler
// This runs in a separate JS context — no React hooks or state allowed here
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
console.log('Background message received:', remoteMessage.messageId);
// You can update AsyncStorage, trigger local notifications, etc.
// Do NOT try to update React state or navigate here
});
AppRegistry.registerComponent(appName, () => App);Quit-state detection (app was closed when notification arrived)
// Check if the app was launched from a notification (quit state)
// Place this in your root navigator or App.tsx
useEffect(() => {
messaging()
.getInitialNotification()
.then((remoteMessage) => {
if (remoteMessage) {
console.log('App launched from notification:', remoteMessage);
handleNotificationNavigation(remoteMessage.data);
}
});
}, []);Notification vs. data messages
FCM has two message types. Notification messages include a notification payload that the OS renders automatically as a banner (including in the background). Data messages contain only a data payload and are always delivered to your handler — no automatic banner. For most apps, use notification messages for user-facing alerts and data messages for silent background syncs.
Deep Linking from Notifications
The most impactful thing you can do with notification tap handling is navigate the user to the relevant screen. Pass a screen key (and optional params) in your FCM data payload, then handle navigation in your tap handlers:
import { NavigationContainerRef } from '@react-navigation/native';
export function handleNotificationNavigation(
data: Record<string, string> | undefined,
navigationRef: NavigationContainerRef<any>
) {
if (!data) return;
const { screen, params } = data;
switch (screen) {
case 'OrderDetail':
navigationRef.navigate('OrderDetail', { orderId: params });
break;
case 'Promotion':
navigationRef.navigate('Promotions', { promoId: params });
break;
default:
navigationRef.navigate('Home');
}
}Your FCM payload would look like: "data": { "screen": "OrderDetail", "params": "order_123" }. Send this payload from your backend or from PushPilot's campaign editor when creating the notification.
Navigation timing on cold launch
getInitialNotification navigation call in a short timeout or wait for the navigator to be mounted — otherwise you'll navigate before the screen stack exists.Connecting to PushPilot
PushPilot acts as your push notification platform — it stores device tokens, manages segments, provides AI-generated notification copy, and sends campaigns via FCM and APNs. Once you have the FCM token from the steps above, register it with PushPilot:
// After getting the FCM token, register it with PushPilot
async function registerWithPushPilot(fcmToken: string, userId?: string) {
await fetch('https://api.pushpilot.ai/v1/devices', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.PUSHPILOT_API_KEY!,
},
body: JSON.stringify({
token: fcmToken,
platform: Platform.OS === 'ios' ? 'apns' : 'fcm',
external_id: userId, // optional: your user identifier
attributes: {
app_version: getAppVersion(),
locale: getLocale(),
},
}),
});
}The external_id field links the device token to your user record in PushPilot. This enables user-level targeting and ensures that if a user installs the app on multiple devices, campaigns reach all of them. Remember to call this function again fromonTokenRefresh to keep tokens up to date.
What PushPilot handles for you
- ✓ Token storage and deduplication
- ✓ Segment management (by attributes, behaviour, cohort)
- ✓ AI-generated notification copy and A/B test variants
- ✓ Campaign scheduling and frequency capping
- ✓ Delivery receipts and open rate analytics
What you keep control of
- • Client-side permission prompting (timing and UX)
- • In-app notification display (foreground handling)
- • Deep link routing logic
- • Silent background data syncs
Testing Your Setup
Use the Firebase console or a direct FCM API call to send a test notification to a specific device token. Log the token to your console during development:
curl -X POST https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
-d '{
"message": {
"token": "YOUR_FCM_TOKEN",
"notification": {
"title": "Test notification",
"body": "If you see this, FCM is working."
},
"data": {
"screen": "Home"
}
}
}'Replace YOUR_PROJECT_ID with your Firebase project ID and YOUR_FCM_TOKEN with the token logged by your app. This lets you test the full message pipeline without deploying a campaign.
Test checklist
Common Issues and Fixes
getToken() throws "No FCM token available"
On iOS, registerDeviceForRemoteMessages() must complete successfully before calling getToken(). On Android, ensure your google-services.json is in the correct location (android/app/) and the Google Services plugin is applied.
Notifications received in background but not foreground
The OS renders background notifications automatically. Foreground notifications require you to call onMessage and display them manually (using a library like react-native-notifee or a custom in-app UI). This is by design — FCM doesn't auto-display notifications when the app is foregrounded.
iOS APNs token not bridging to FCM token
Check that your GoogleService-Info.plist is correctly added to the Xcode target (not just the project). Also verify the Push Notifications capability is enabled under Signing & Capabilities. Rebuild fully after any entitlements change.
Background handler not called on Android
setBackgroundMessageHandler must be registered in index.js at the very top level — before any React component registration. If it's inside a component or effect, it won't be registered when the app is in the background and Firebase spawns a headless task.
Notifications not showing on Android 13+ device
Android 13 (API 33) introduced a runtime notification permission. Your app must call messaging().requestPermission() and the user must grant it. The POST_NOTIFICATIONS manifest entry alone is insufficient.
FAQs
Can I use react-native-firebase with Expo?
Yes, but only with the Expo bare workflow (you've run expo eject or started with npx react-native init). Expo managed workflow requires expo-notifications instead.
Do I need a backend to store FCM tokens?
Not necessarily. If you're using a push notification platform like PushPilot, you register the token directly via their API and the platform handles storage, targeting, and sending. You only need your own backend token storage if you're sending notifications directly through the FCM HTTP API without a platform.
How do I handle notifications for logged-out users?
FCM tokens exist at the device-installation level, independent of user authentication. If a user logs out, you should disassociate the token from their user ID in your backend (or update the external_id in PushPilot to null or anonymous). When they log back in, re-associate the current token with the new session.
What's the difference between FCM and APNs?
FCM (Firebase Cloud Messaging) is Google's push delivery service, used for Android devices. APNs (Apple Push Notification service) is Apple's service for iOS. For React Native apps, Firebase bridges both: your server sends a notification to FCM, and FCM forwards it to APNs for iOS devices. You only need to interact with the FCM API — Firebase handles the APNs routing, as long as you've uploaded your APNs key to the Firebase console.
How do I send notifications to all users vs. specific segments?
Using the raw FCM API, you need to maintain a list of tokens and send to each individually (FCM no longer supports topic broadcasting to all devices reliably). Using PushPilot, you create segments based on user attributes or behaviour and target those segments directly from the campaign dashboard — no custom backend code required.
Try it free
Ready to automate your push notifications?
Connect Firebase or OneSignal in clicks. Describe a campaign. Wake up to fresh notifications, sent.