TL;DR: This guide provides a production-ready approach to implementing Firebase Cloud Messaging (FCM) in React Native using the
@react-native-firebase/messagingpackage. It covers essential cross-platform configurations, including explicitly requesting thePOST_NOTIFICATIONSpermission for Android 13+ and setting up iOS using a non-expiring.p8APNs Auth Key instead of legacy.p12certificates.
⚡ Key Takeaways
- Maintain exact version parity between
@react-native-firebase/appand@react-native-firebase/messagingin yourpackage.jsonto prevent obscure native compilation errors. - Apply the
com.google.gms.google-servicesplugin in yourandroid/app/build.gradleand place thegoogle-services.jsonfile in theandroid/app/directory to link Firebase to Android. - Add the
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />tag to yourAndroidManifest.xmlto support Android 13 (API level 33). - Use React Native's
PermissionsAndroid.requestto explicitly prompt users for thePOST_NOTIFICATIONSpermission on Android devices running version 33 or higher. - Use a
.p8APNs Auth Key instead of legacy.p12certificates for iOS to create a single, non-expiring configuration for both development and production environments.
Adding push notifications to a React Native app sounds straightforward: install a library, copy some setup code, and watch the messages roll in.
But if you've ever implemented them in production, you know the reality. You follow the documentation perfectly, yet iOS silently drops your messages because of a mismatched Apple Push Notification service (APNs) certificate, or Android 13's new permission model blocks your alerts. Worst of all, your background handlers randomly crash because the headless JavaScript task wasn't registered in the correct app lifecycle state. The result? Your users miss critical updates, and your engagement metrics take a hit.
The solution isn't another hacky workaround; it's a methodical, architecturally sound implementation of Firebase Cloud Messaging (FCM) using the official @react-native-firebase/messaging package.
In this guide, we are going to bypass the beginner-level tutorials. We'll walk through a production-grade FCM configuration, focusing heavily on cross-platform quirks, handling Android 13 permissions, mapping background versus foreground lifecycles, and managing device tokens properly.
Core Setup: Installing React Native Firebase
Before touching notifications, you need the core Firebase app module alongside the messaging module. We assume you have already created a Firebase project in the console and have your bare React Native project ready.
Run the following command to install the required dependencies:
yarn add @react-native-firebase/app @react-native-firebase/messaging
Once installed, you must link the native iOS dependencies:
cd ios && pod install && cd ..
Production Note: Always ensure your
@react-native-firebasepackages share the exact same version number. Mismatched versions between theappandmessagingpackages are a leading cause of obscure native compilation errors.
In our React Native mobile app development services, we strictly enforce version parity in package.json to prevent CI/CD pipeline failures.
Android Configuration and Android 13+ Permissions
Android requires the google-services.json file downloaded from your Firebase console. Place this file inside your android/app/ directory.
Next, enable the Google Services plugin. Update your root android/build.gradle:
buildscript {
dependencies {
// Add this line
classpath 'com.google.gms:google-services:4.4.0'
}
}
Then, apply the plugin at the top of your android/app/build.gradle:
apply plugin: "com.android.application"
// Add this line
apply plugin: "com.google.gms.google-services"
Handling the Android 13 Permission Shift
Historically, Android granted push notification permissions automatically upon installation. Starting with Android 13 (API level 33), you must explicitly request the POST_NOTIFICATIONS permission.
First, declare the permission in your android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- Add this line for Android 13+ -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- ... -->
</manifest>
Then, handle the permission request in your React Native code. We typically abstract this into a utility function that gets called during the user onboarding flow:
import { PermissionsAndroid, Platform } from 'react-native';
export async function requestAndroidPushPermission() {
if (Platform.OS === 'android' && Platform.Version >= 33) {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
{
title: 'Notification Permission',
message: 'We need your permission to send you critical alerts.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
},
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
console.log('Android push permission granted');
return true;
} else {
console.log('Android push permission denied');
return false;
}
}
return true; // Auto-granted on older Android versions
}
Conquering iOS APNs and Capabilities
Firebase doesn't magically bypass Apple's infrastructure; it acts as a proxy for APNs. To make this work, you must link your Apple Developer account to Firebase.
Instead of dealing with legacy .p12 certificates that expire annually, generate an APNs Auth Key (.p8 file) in your Apple Developer portal. Upload this single .p8 key to the Firebase Console (Project Settings -> Cloud Messaging -> Apple app configuration). It works for both development and production environments and never expires.
Next, open Xcode, select your project target, navigate to the Signing & Capabilities tab, and add the following:
- Push Notifications
- Background Modes -> Check "Remote notifications" and "Background fetch"
Finally, iOS requires explicit user consent via the Firebase SDK. We handle this alongside the Android permission:
import messaging from '@react-native-firebase/messaging';
import { Platform } from 'react-native';
export async function requestIOSPushPermission() {
if (Platform.OS === 'ios') {
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) {
console.log('iOS push permission granted. Status:', authStatus);
}
return enabled;
}
return true;
}
Implementing Foreground and Background Message Handlers
This is where many intermediate developers stumble. React Native utilizes two distinct lifecycles for push notifications: foreground (when the app is visible on the screen) and background/quit (when the app is minimized or completely killed).
The Headless JS Background Handler
When your app is closed, React Native spins up a Headless JS task to process the incoming FCM payload. This handler must be registered early in your application lifecycle, entirely outside of your component tree—typically in index.js.
// index.js
import { AppRegistry } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import App from './App';
import { name as appName } from './app.json';
// Register background handler early
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
console.log('Message handled in the background!', remoteMessage.messageId);
// Example: Update local SQLite database or AsyncStorage here
// Do NOT attempt to update UI state here!
});
AppRegistry.registerComponent(appName, () => App);
Warning: Never use UI components, React hooks, or Alerts inside
setBackgroundMessageHandler. The React component tree does not exist when this function runs.
The Foreground Handler
When the user is actively using the app, notifications will not appear in the system tray by default. Instead, FCM triggers the onMessage listener. You are responsible for showing an in-app banner or updating the UI manually.
Here is how we handle foreground notifications using a custom React hook:
// hooks/useFCM.js
import { useEffect } from 'react';
import { Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';
export function useForegroundFCM() {
useEffect(() => {
const unsubscribe = messaging().onMessage(async (remoteMessage) => {
// In production, use a custom toast UI library rather than Alert
Alert.alert(
remoteMessage.notification?.title || 'New Message',
remoteMessage.notification?.body || 'You received a notification.'
);
console.log('Foreground message received:', remoteMessage.data);
});
// Clean up the listener on unmount
return unsubscribe;
}, []);
}
When we built the real-time dispatch tracking system for Driftload, ensuring that foreground messages updated the live map without dropping a single frame was a massive priority. Separating the background logic from the foreground UI updates is what makes that reliability possible.
Architecting FCM Token Management
To send a push notification to a specific user, your backend needs their unique FCM device token. These tokens are not static; they rotate when the app is restored on a new device, when the user uninstalls and reinstalls the app, or when Firebase forces a refresh.
A robust token management strategy involves capturing the token on login and actively listening for token refreshes.
import messaging from '@react-native-firebase/messaging';
import api from '../services/api'; // Your custom backend API service
export const syncFCMToken = async (userId) => {
try {
// 1. Get the current token
const token = await messaging().getToken();
await api.post('/users/fcm-token', { userId, token });
// 2. Listen for future token refreshes
messaging().onTokenRefresh(async (newToken) => {
console.log('FCM Token refreshed:', newToken);
await api.post('/users/fcm-token', { userId, token: newToken });
});
} catch (error) {
console.error('Failed to sync FCM token:', error);
}
};
Your backend should maintain a one-to-many relationship between Users and Devices. When a user logs out, ensure you send a request to your API to invalidate or delete that specific FCM token. This prevents notifications from being delivered to someone who is no longer authenticated on that device.
Handling Notification Interactions and Deep Linking
Users don't just look at notifications; they tap them. Your app needs to know why it was opened so it can route the user to the correct screen (e.g., opening a specific chat thread or order details view).
There are two routing scenarios to handle:
- The app was running in the background and brought to the foreground (
onNotificationOpenedApp). - The app was completely killed and launched entirely by the tap (
getInitialNotification).
import React, { useEffect } from 'react';
import messaging from '@react-native-firebase/messaging';
import { useNavigation } from '@react-navigation/native';
export function NotificationRouter() {
const navigation = useNavigation();
useEffect(() => {
// Scenario 1: App opened from a background state
const unsubscribe = messaging().onNotificationOpenedApp((remoteMessage) => {
console.log('App opened from background by notification:', remoteMessage.notification);
handleNavigation(remoteMessage.data);
});
// Scenario 2: App launched from a quit state
messaging()
.getInitialNotification()
.then((remoteMessage) => {
if (remoteMessage) {
console.log('App launched by notification:', remoteMessage.notification);
handleNavigation(remoteMessage.data);
}
});
return unsubscribe;
}, []);
const handleNavigation = (data) => {
if (data?.type === 'ORDER_UPDATE' && data?.orderId) {
navigation.navigate('OrderDetails', { orderId: data.orderId });
}
};
return null;
}
Structuring the Backend Payload for Cross-Platform Reliability
A frequent source of developer frustration occurs when background handlers work perfectly on Android but fail silently on iOS. This almost always comes down to how your backend constructs the FCM JSON payload.
FCM payloads contain two main objects: notification (handled automatically by the system UI) and data (custom key-value pairs handled by your JavaScript code).
To ensure your React Native background handler is triggered on iOS, your backend must include "content-available": 1 in the APNs configuration.
Here is an example of a properly structured Node.js backend payload using the firebase-admin SDK:
// Backend (Node.js) using firebase-admin
const admin = require('firebase-admin');
const message = {
token: 'user_device_fcm_token_here',
notification: {
title: 'Your order has shipped!',
body: 'Track your delivery in real-time.',
},
data: {
type: 'ORDER_UPDATE',
orderId: 'ORD-98765',
},
android: {
priority: 'high',
},
apns: {
payload: {
aps: {
'content-available': 1, // CRITICAL for iOS background tasks
sound: 'default',
},
},
},
};
admin.messaging().send(message)
.then((response) => console.log('Successfully sent message:', response))
.catch((error) => console.log('Error sending message:', error));
By explicitly defining platform-specific configurations (android.priority and apns.payload.aps['content-available']), you guarantee that both operating systems treat the notification as a high-priority event capable of waking up your headless JS task.
Debugging Common Production Issues
Even with a perfect setup, production environments will throw curveballs. Here are the first things to check when push notifications stop working:
- Tokens aren't routing: Verify if you are accidentally using an APNs token instead of an FCM token on your backend. The
@react-native-firebase/messaginglibrary abstracts this, but if you mix in custom native Swift/Kotlin code, you might cross the streams. - Notifications don't show when the app is open: This is by design. If you want a visual banner while the app is in the foreground, you must build it manually inside the
onMessagelistener using a library likereact-native-toast-message. - iOS Release Build Failures: If notifications work in Development but fail in TestFlight, check your APNs
.p8key in the Firebase Console. Ensure your Xcode project's Bundle Identifier matches the one registered in the Apple Developer Portal and Firebase perfectly.
Building a resilient mobile architecture requires a deep understanding of native device lifecycles. If your team is struggling with unreliable push notifications, convoluted state management, or complex React Native architecture, you can book a free architecture review with our engineers. We specialize in untangling mobile tech debt and getting products to market flawlessly.
Need help building this in production?
SoftwareCrafting is a full-stack dev agency — we ship fast, scalable React, Next.js, Node.js, React Native & Flutter apps for global clients.
Get a Free ConsultationFrequently Asked Questions
How do I handle push notification permissions for Android 13+ in React Native?
Starting with Android 13 (API level 33), push notifications are no longer auto-granted upon installation. You must declare the POST_NOTIFICATIONS permission in your AndroidManifest.xml and explicitly request it using React Native's PermissionsAndroid API during your user onboarding flow.
Should I use a .p12 certificate or a .p8 Auth Key for iOS Firebase push notifications?
You should use an APNs Auth Key (.p8 file) instead of legacy .p12 certificates. A .p8 key never expires and works seamlessly for both development and production environments, making it the recommended approach for linking your Apple Developer account to Firebase.
Why am I getting native compilation errors after installing React Native Firebase?
A leading cause of obscure native compilation errors is mismatched version numbers between @react-native-firebase/app and @react-native-firebase/messaging. Always ensure all your React Native Firebase packages share the exact same version number in your package.json before running pod install.
How can I ensure my React Native Firebase integration doesn't break my CI/CD pipelines?
The most critical step is strictly enforcing version parity across all Firebase modules to prevent build failures. Our team at SoftwareCrafting provides expert React Native mobile app development services that implement automated checks and robust architectures to ensure stable, production-grade dependency management.
What are the core dependencies needed to implement FCM in React Native?
You need to install both the core @react-native-firebase/app module and the @react-native-firebase/messaging module. After adding them via your package manager, you must run pod install in your iOS directory to properly link the native dependencies.
Can I get professional help fixing silent notification drops or background handler crashes in my app?
Yes, debugging cross-platform notification quirks and headless JavaScript tasks can be incredibly time-consuming. Through SoftwareCrafting's React Native mobile app development services, we can audit your app's lifecycle states, fix APNs mismatches, and build a reliable, production-ready FCM architecture for your users.
📎 Full Code on GitHub Gist: The complete
commands-1.shfrom this post is available as a standalone GitHub Gist — copy, fork, or embed it directly.
