Wiring up FCM cleanly across iOS, Android and React Native
Push notifications are not a single, uniform feature but an interplay of APNs, Firebase Cloud Messaging, operating system permissions and app state. Anyone who wires up FCM in React Native without a clean setup for foreground, background and quit state loses notifications exactly when users need them most. This article shows how the Firebase project, the FCM SDK, permissions, payloads, deep linking and token management fit together reliably in React Native.
Table of Contents
- 1. Why push notifications are a cross-platform challenge
- 2. Setting up a Firebase project
- 3. FCM SDK setup in React Native
- 4. Requesting permissions
- 5. Foreground, background and quit state handling
- 6. Designing notification payloads
- 7. Deep linking from notifications
- 8. Token management, topics and segmentation
- 9. FCM compared to Expo Notifications and OneSignal
- 10. Summary
- 11. FAQ
1. Why push notifications are a cross-platform challenge
Underneath the surface, push notifications hide two completely separate delivery systems. On iOS, every message ultimately travels through the Apple Push Notification service, APNs, a proprietary protocol with its own certificate management and its own payload limits. On Android, Firebase Cloud Messaging handles delivery directly through Google infrastructure. FCM itself merely forwards messages destined for iOS devices on to APNs, acting as an intermediary there, while it is the primary channel for Android. Anyone who ignores this split and expects a single, platform-independent behavior quickly runs into inconsistencies around delivery timing, payload structure and prioritization.
To make things harder, the app state at the moment of delivery differs fundamentally, and each platform reacts differently. An app in the foreground receives the message as a pure data event, without the operating system automatically showing a visible notification. An app in the background lets the operating system take care of the display, provided the message arrives as a notification message rather than a pure data message. A fully terminated app, the so called quit state, can be handled differently on iOS and Android, especially once the user has force quit the app, after which some delivery paths are deliberately blocked by Google and Apple.
For React Native, a third layer comes into play: the bridge between native SDKs and JavaScript. Both APNs and FCM deliver their events natively first, before @react-native-firebase/messaging forwards them to app logic as JavaScript events. This detour means listeners must be registered at the right moment, usually before the React component tree has even finished initializing, since messages that arrive while the app is still booting up would otherwise be lost.
2. Setting up a Firebase project
The foundation for Firebase Cloud Messaging is a Firebase project in the Firebase Console, to which a separate app registration is added for each target platform. For Android, the app is registered using the package name, which must match the applicationId in android/app/build.gradle exactly. For iOS, registration happens through the bundle ID, which in turn must exactly match the bundle identifier configured in Xcode. Any mismatch, even a single letter of casing, means FCM messages will fail to reach the app later on, without a meaningful error ever surfacing.
For Android, the Firebase Console downloads a google-services.json, which gets copied into android/app/ and contains the Firebase project configuration, including API key and sender ID. For iOS, the counterpart is GoogleService-Info.plist, which must be added to the target inside the Xcode project, not merely copied into the file system, since Xcode otherwise won't include it in the app bundle. On top of that, the APNs authentication key must be uploaded to the Firebase project for iOS, a .p8 file from the Apple Developer Portal, without which FCM accepts messages but can never forward them to Apple's APNs.
{
"project_info": {
"project_id": "mironsoft-push-demo",
"storage_bucket": "mironsoft-push-demo.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:123456789012:android:abc123def456",
"android_client_info": {
"package_name": "de.mironsoft.pushdemo"
}
},
"api_key": [
{ "current_key": "AIzaSyDUMMY-REPLACE-WITH-REAL-KEY" }
]
}
],
"configuration_version": "1"
}
3. FCM SDK setup in React Native
The React Native Firebase community fork, by now the officially recommended module for Firebase integration, is split across several packages, of which @react-native-firebase/app is required as the base module. It initializes the native Firebase app instance from google-services.json and GoogleService-Info.plist and must be loaded before any other Firebase package. Building on top of it, @react-native-firebase/messaging delivers the actual FCM functionality: token retrieval, listeners for incoming messages, and permission handling.
On iOS, a pod install inside the ios/ directory is required after installing the packages, since the native Firebase SDKs are pulled in through CocoaPods. In Xcode, the target's capabilities must also have Push Notifications and Background Modes enabled, with Remote Notifications checked, without which the operating system discards incoming FCM messages in the background. On Android, from Firebase Messaging version 23 onward, the Google Services Gradle plugin is additionally required in android/build.gradle and android/app/build.gradle, which processes google-services.json at build time.
A frequently overlooked step is registering the background message handler outside of any React component, directly at the app's entry point, usually in index.js, before AppRegistry.registerComponent. If the handler is instead registered inside a component, it misses messages that arrive before the component mounts, which regularly causes silent failures, particularly on app launch from the quit state.
# Install the core Firebase module and the messaging module
npm install @react-native-firebase/app @react-native-firebase/messaging
# iOS: install native CocoaPods dependencies
cd ios && pod install && cd ..
# Android: no extra step needed beyond the Gradle plugin
# already wired into android/build.gradle and app/build.gradle
# Verify the installed native modules are linked correctly
npx react-native run-ios
npx react-native run-android
4. Requesting permissions
On iOS, the permission request has always been explicit: without a call to messaging().requestPermission(), the operating system never shows a notification, even if FCM technically delivers the message correctly. The return value distinguishes between AUTHORIZED, PROVISIONAL, for silent delivery into the notification center without a banner, and DENIED. Ideally, the call should be tied to a sensible moment during onboarding rather than fired immediately on first app launch, since a once denied permission cannot be re-prompted through a dialog without the user manually going through the iOS settings.
Once permission has been granted, the JavaScript call alone is still not enough on iOS. The native AppDelegate additionally needs to register for remote notifications with Apple and forward the received APNs device token to Firebase, so that FCM can address this device at all. Without this native forwarding step, the Firebase token remains valid, but the link between the APNs identity and the Firebase registration is missing, and messages simply go nowhere.
// AppDelegate.swift - forward the APNs device token to Firebase for FCM
import FirebaseMessaging
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
// Hand the raw APNs token over to Firebase so FCM can route messages
Messaging.messaging().apnsToken = deviceToken
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("APNs registration failed: \(error.localizedDescription)")
}
On Android, up through version 12, no explicit runtime permission for notifications was needed at all, apps were allowed to show notifications silently. Since Android 13 (API level 33), POST_NOTIFICATIONS is a regular dangerous permission that must be requested at runtime through PermissionsAndroid.request(), in addition to the manifest entry. If the permission is not actively requested on Android 13+, FCM still delivers data messages in the background, but suppresses any visible notification display entirely, a state that is easy to miss during testing because the app appears to work normally.
5. Foreground, background and quit state handling
For each of the three app states, @react-native-firebase/messaging provides its own listener, and all three must be handled independently. messaging().onMessage() fires while the app is running in the foreground. In this case the operating system does not show a visible notification even when the incoming message is marked as a notification message, which is intentional design: the app itself should decide whether and how to display a hint, for example as an in-app banner instead of a system notification. messaging().setBackgroundMessageHandler() takes over messages while the app is in the background but still kept in memory, running natively, isolated from the active JavaScript instance of the visible app.
The quit state is the most complicated case. If the app has been fully terminated, messaging().getInitialNotification() must be queried on app launch to determine whether the launch was triggered by a tap on a notification. Important: Apple and Google treat a user initiated force quit differently from a system initiated background termination. After a force quit, Android in certain configurations stops delivering FCM messages entirely until the app is manually reopened, a behavior that cannot be worked around from app code and must be factored into expectations around delivery rates.
All three listeners should be registered as early as possible, ideally in a central initialization function that is called both on app launch and whenever the app returns to the foreground. A common mistake is registering the foreground listener only after the first render of a deeply nested component, which loses messages that arrive in exactly that short window.
// notifications/setup.js - register listeners for all three app states
import messaging from '@react-native-firebase/messaging';
// Background and quit-state handler, registered outside any component,
// typically in index.js before AppRegistry.registerComponent
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
console.log('Message handled in the background:', remoteMessage.messageId);
});
export function registerForegroundListeners(onNotificationTap) {
// Foreground messages: OS does not show a system banner automatically
const unsubscribeOnMessage = messaging().onMessage(async (remoteMessage) => {
console.log('Foreground message received:', remoteMessage.notification);
// Show an in-app banner or toast here instead of relying on the OS
});
// App opened from background state by tapping a notification
const unsubscribeOpenedApp = messaging().onNotificationOpenedApp((remoteMessage) => {
onNotificationTap(remoteMessage);
});
// App opened from quit state by tapping a notification
messaging()
.getInitialNotification()
.then((remoteMessage) => {
if (remoteMessage) {
onNotificationTap(remoteMessage);
}
});
return () => {
unsubscribeOnMessage();
unsubscribeOpenedApp();
};
}
6. Designing notification payloads
An FCM message can be sent as a notification message, a data message, or a combination of both, and this choice determines the behavior in every app state. A pure notification message with a notification object lets the operating system take over the display automatically when the app is in the background or terminated, but in the foreground it only fires the onMessage listener without any visible display. A pure data message with a data object and no notification field reaches the app in every state as a plain JavaScript event, but never shows anything automatically, the app has to take care of the display itself, for example through a local notification library.
For most production systems, combining both fields is the most pragmatic approach: the notification object handles the automatic display in the background and quit state, while the accompanying data object carries additional metadata such as the target screen, the ID of a referenced record, or a category, which gets evaluated on tap for deep linking. The hard payload limit matters here: FCM caps the total size of a message at 4 kilobytes, which is not enough for images or larger structured data, so such content must be referenced by URL and fetched only after the notification has been received.
Priority is another lever: on Android, FCM distinguishes between normal and high priority, and only high priority guarantees that doze mode and app standby don't delay delivery. On iOS, the APNs content-available field, combined with silent delivery, controls whether a message is treated as a background fetch trigger without the user ever seeing a visible notification, a mechanism well suited to silent data synchronization but rate limited by the system and therefore not reliable for time critical content.
7. Deep linking from notifications
The actual value of a push notification often only materializes through the tap: the user should not just open the app but land directly on the relevant screen, for example a specific order, a chat thread, or a product detail page. The deep linking needed for this builds on the same listeners already registered for app state handling: onNotificationOpenedApp for a tap from the background and getInitialNotification for a tap from the quit state. Both deliver the full remoteMessage object, including the data field from which the target route is extracted.
The actual navigation runs through a reference to the React Navigation container, usually via createNavigationContainerRef(), because at the moment of a notification tap from the quit state the navigation structure might not be fully mounted yet. A robust pattern defers the actual navigation until the container is truly ready, instead of firing it immediately without checking, since an overly early navigation call would otherwise silently do nothing, leaving the user on the home screen despite the tap.
// navigation/notificationLinking.js - deep link into a specific screen on tap
import { createNavigationContainerRef } from '@react-navigation/native';
export const navigationRef = createNavigationContainerRef();
function navigateWhenReady(name, params) {
if (navigationRef.isReady()) {
navigationRef.navigate(name, params);
return;
}
// Retry shortly after mount if the navigator is not ready yet
setTimeout(() => navigateWhenReady(name, params), 300);
}
export function handleNotificationTap(remoteMessage) {
const { screen, referenceId } = remoteMessage.data ?? {};
switch (screen) {
case 'order-detail':
navigateWhenReady('OrderDetail', { orderId: referenceId });
break;
case 'chat-thread':
navigateWhenReady('ChatThread', { threadId: referenceId });
break;
default:
navigateWhenReady('Home');
}
}
8. Token management, topics and segmentation
Every device installation receives a unique registration token from FCM through messaging().getToken(), which serves as the target address for server triggered messages aimed at exactly that one device. This token is not permanently stable: it can change on reinstallation, when app data is cleared, or through internal rotation by Firebase, which is why messaging().onTokenRefresh() must be registered to push the current token to your own backend immediately on every change. A stale, unrefreshed token leads to silent delivery failures that often only become visible in the Firebase dashboard with a delay.
For broadcast messages to larger user groups, rather than individual devices, topics are the right tool. With messaging().subscribeToTopic('promotions'), a device subscribes to a topic, to which messages can subsequently be sent server side without knowing individual tokens. Topics work well for broad, lightly personalized categories such as product announcements or maintenance windows, but they are unsuitable for individualized segmentation, for example by purchase history or user behavior, since FCM itself offers no server side filtering logic per topic.
For finer segmentation, such as users with an expired subscription or cart abandoners, the only path is a custom database table that links tokens to user attributes, together with server side batch sending to filtered token lists via the Firebase Admin SDK. Important here: tokens reported as invalid by Firebase, for example after the app has been uninstalled, must be actively removed from your own database, otherwise the delivery list keeps growing with dead entries that artificially worsen the delivery rate shown in analytics.
9. FCM compared to Expo Notifications and OneSignal
Firebase Cloud Messaging is not the only option for push notifications in React Native. Expo Notifications wraps FCM and APNs behind its own simplified API and its own push token format, while OneSignal, as a specialized third party service, layers additional segmentation and analytics features on top of FCM and APNs. The following table compares the three approaches along practically relevant dimensions.
| Dimension | Firebase Cloud Messaging | Expo Notifications | OneSignal |
|---|---|---|---|
| Setup complexity | Moderate, requires its own Firebase project and native configuration | Low, a single Expo push token for both platforms | Low to moderate, own dashboard and SDK integration |
| Bare workflow requirement | Yes, native modules and google-services.json are required | No, works inside the Expo Managed Workflow | Yes for full feature set, an Expo plugin is available |
| Segmentation | Topics only, no server side filtering logic | No built-in segmentation, individual tokens only | Extensive, segments by behavior and attributes |
| Analytics | Basic, via the Firebase Console | Minimal, no dedicated analytics dashboard | Detailed, delivery rates, open rates, A/B tests |
| Pricing | Free with no volume cap | Free, technically runs on top of FCM/APNs | Free tier limited, paid plans once you scale |
In practice, Firebase Cloud Messaging is the right choice when a bare React Native workflow and a custom backend already exist and full control over payloads and delivery behavior is desired. Expo Notifications suits teams that stay in the Managed Workflow and cover simple use cases without complex segmentation. OneSignal pays off once marketing teams want to run campaigns and A/B tests independently without developer involvement, though it comes with ongoing costs and an additional dependency alongside FCM itself.
Mironsoft
React Native push infrastructure and mobile engagement setups
Push notifications that reliably arrive, no matter the app state?
We set up Firebase Cloud Messaging cleanly for iOS and Android, wire up foreground, background and quit state handling correctly, and build deep linking and topic segmentation that stay stable as you scale.
FCM setup
Firebase project, APNs certificates and @react-native-firebase/messaging wired up correctly
App state handling
Foreground, background and quit state reliably covered, including deep linking
Tokens & segmentation
Token rotation, topic subscriptions and backend integration for scalable delivery
10. Summary
Push notifications with Firebase Cloud Messaging work reliably in React Native once the two separate delivery systems, APNs for iOS and FCM for Android, along with the three app states foreground, background and quit state, are deliberately handled apart. The Firebase project with google-services.json, GoogleService-Info.plist and the APNs authentication key forms the foundation, while @react-native-firebase/messaging provides the JavaScript interface for permissions, listeners and token management.
The payload choice between notification and data message determines the behavior in every state, while deep linking through onNotificationOpenedApp and getInitialNotification turns a mere hint into a genuine entry point into the app. Topics cover broad broadcast scenarios, while finer segmentation requires a custom token database or a specialized service like OneSignal. Assemble these building blocks cleanly, and you get push notifications that actually arrive in practice, rather than being merely delivered in theory.
Push Notifications with Firebase Cloud Messaging, the essentials at a glance
Two delivery systems
APNs for iOS, FCM for Android. FCM merely forwards iOS messages on to APNs instead of delivering them directly.
Three app states
onMessage, setBackgroundMessageHandler and getInitialNotification cover foreground, background and quit state.
Time-sensitive permissions
iOS: request requestPermission() explicitly. Android 13+: POST_NOTIFICATIONS as a mandatory runtime permission.
Token and topic upkeep
Register onTokenRefresh, remove invalid tokens, use topics for broadcasts, a custom database for segmentation.