Push notifications are one of the most requested features in mobile apps and one of the most error-prone to implement. Between Android's notification channels, iOS entitlements, background state handling, and the APNs/FCM split, there are a lot of moving parts. This guide covers the full setup from Firebase project creation to production-ready notification handling.
FCM vs APNs — What's the Difference?
APNs (Apple Push Notification service) is Apple's delivery infrastructure. FCM (Firebase Cloud Messaging) is Google's. On Android, FCM delivers directly. On iOS, FCM acts as a relay — your server sends to FCM, which forwards to APNs, which delivers to the device. Most React Native projects use FCM as a single endpoint for both platforms.
Setting Up Firebase
- Create a project at console.firebase.google.com.
- Download
google-services.json(Android) andGoogleService-Info.plist(iOS). - Place them at
android/app/google-services.jsonandios/MyApp/GoogleService-Info.plist.
npm install @react-native-firebase/app @react-native-firebase/messaging
In android/build.gradle, add the Google services classpath:
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.4.0'
}
}
In android/app/build.gradle, apply the plugin at the bottom:
apply plugin: 'com.google.gms.google-services'
On iOS, add the Push Notifications capability in Xcode under Signing & Capabilities, then upload your APNs key or certificate to the Firebase console under Project Settings > Cloud Messaging.
Requesting Permissions
import messaging from '@react-native-firebase/messaging';
import { Platform, Alert } from 'react-native';
export async function requestNotificationPermission() {
if (Platform.OS === 'ios') {
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (!enabled) {
Alert.alert(
'Notifications disabled',
'Enable notifications in Settings to receive updates.',
);
return false;
}
}
// Android 13+ requires runtime permission
if (Platform.OS === 'android' && Platform.Version >= 33) {
const { PermissionsAndroid } = require('react-native');
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
);
if (granted !== PermissionsAndroid.RESULTS.GRANTED) return false;
}
return true;
}
Getting and Storing the FCM Token
export async function registerDevice() {
await messaging().registerDeviceForRemoteMessages();
const token = await messaging().getToken();
// Send token to your backend for storage
await api.updateDeviceToken(token);
// Refresh token when it changes
return messaging().onTokenRefresh(async (newToken) => {
await api.updateDeviceToken(newToken);
});
}
Handling the Three Notification States
A notification can arrive when the app is in one of three states. Each requires different handling:
import messaging from '@react-native-firebase/messaging';
// 1. FOREGROUND — app is open and active
messaging().onMessage(async (remoteMessage) => {
// FCM does NOT show a system notification in foreground automatically
// Display your own in-app banner or use notifee for a local notification
showInAppBanner(remoteMessage.notification);
});
// 2. BACKGROUND — notification tapped while app was in background
messaging().onNotificationOpenedApp((remoteMessage) => {
navigateFromNotification(remoteMessage.data);
});
// 3. QUIT STATE — app was fully closed
messaging()
.getInitialNotification()
.then((remoteMessage) => {
if (remoteMessage) navigateFromNotification(remoteMessage.data);
});
// Background handler for data-only messages (must be outside component tree)
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
console.log('Background message:', remoteMessage);
});
Android Notification Channels
Android 8+ requires all notifications to be assigned to a channel. Users can silence individual channels in system settings. Use notifee to manage channels:
import notifee, { AndroidImportance } from '@notifee/react-native';
export async function createNotificationChannels() {
await notifee.createChannel({
id: 'alerts',
name: 'Critical Alerts',
importance: AndroidImportance.HIGH,
sound: 'default',
vibration: true,
});
await notifee.createChannel({
id: 'marketing',
name: 'Promotions',
importance: AndroidImportance.LOW,
});
}
Deep Linking from Notifications
Include a screen and params in the FCM data payload from your server:
{
"data": {
"screen": "OrderDetail",
"orderId": "ORD-12345"
}
}
function navigateFromNotification(data) {
if (!data?.screen) return;
// navigationRef is a ref attached to NavigationContainer
navigationRef.current?.navigate(data.screen, { id: data.orderId });
}
Testing Push Notifications
- Firebase Console: Cloud Messaging > Send test message > paste your FCM token.
- Android CLI:
adb shell am broadcast -a com.google.android.c2dm.intent.RECEIVE ... - iOS Simulator:
xcrun simctl push <device-id> <bundle-id> payload.apns
Push notifications are one of those features where edge cases live in production. Test all three states — foreground, background, and quit — before every release. A notification that deep-links to a crashed screen is worse than no notification at all.

