Blog
INTEGRATIONSENGINEERING

React Native Push Notifications: FCM, Expo, and Production-Ready

Kyle Seyler

January 07, 2026

React Native and Courier

Table of contents

React Native Push Notifications: FCM, Expo, and Production-Ready

React Native lets you ship to iOS and Android from a single codebase—and that efficiency should extend to your notification system. But implementing notifications that work reliably across both platforms, scale with your user base, and give users control requires more than wiring up Firebase.

This guide covers how to implement push notifications in React Native, why the platform matters for notification strategy, and how to build a complete notification system that goes beyond basic push.

Table of Contents


Why React Native for Mobile Notifications

React Native powers apps for Meta, Microsoft, Shopify, and Discord. For notifications specifically, it offers real advantages.

🔄 Single codebase, unified logic. Your notification handling lives in one place. Add a notification type or update deep linking once, not twice.

⚡ JavaScript ecosystem. Your mobile code can share patterns with your web app. Teams using Node.js backends find React Native familiar.

🚀 Faster iteration. Hot reloading and over-the-air updates mean you can iterate on notification experiences without app store approvals.

The catch: while React Native unifies your application code, push notifications still require platform-specific infrastructure. iOS uses APNs, Android uses FCM, and you need both configured correctly.


Implementing Push Notifications in React Native

Let's implement push notifications from scratch, then show how an orchestration layer simplifies the production path.

Setting Up Firebase Cloud Messaging

FCM is the standard foundation—it handles Android natively and routes to APNs for iOS.

Copied!

yarn add @react-native-firebase/app
yarn add @react-native-firebase/messaging
# iOS: install pods
cd ios/ && pod install

You'll need platform-specific configuration:

  • Android: Add google-services.json to your project
  • iOS: Configure APNs certificates and add GoogleService-Info.plist

Requesting Notification Permissions

iOS requires explicit permission. Request it with context about why notifications are valuable:

Copied!

import messaging from '@react-native-firebase/messaging';
async function requestNotificationPermission() {
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) {
const token = await messaging().getToken();
await saveTokenToBackend(token);
}
return enabled;
}

Managing Device Tokens

Tokens change when users reinstall apps, restore from backup, or when the OS rotates them:

Copied!

import { useEffect } from 'react';
import messaging from '@react-native-firebase/messaging';
function useNotificationSetup(userId) {
useEffect(() => {
messaging()
.getToken()
.then(token => saveTokenToBackend(userId, token));
const unsubscribe = messaging().onTokenRefresh(token => {
saveTokenToBackend(userId, token);
});
return unsubscribe;
}, [userId]);
}

Handling Notification Events

React Native apps handle notifications in three states:

Copied!

import messaging from '@react-native-firebase/messaging';
import { useEffect } from 'react';
function useNotificationHandlers(navigation) {
useEffect(() => {
// App in foreground
const unsubscribeForeground = messaging().onMessage(async message => {
showInAppNotification(message);
});
// App in background - user tapped notification
const unsubscribeBackground = messaging().onNotificationOpenedApp(message => {
handleNotificationNavigation(message, navigation);
});
// App was closed - opened via notification
messaging()
.getInitialNotification()
.then(message => {
if (message) {
handleNotificationNavigation(message, navigation);
}
});
return () => {
unsubscribeForeground();
unsubscribeBackground();
};
}, [navigation]);
}

This gets push working. But production apps quickly hit limitations: no delivery confirmation, no fallback channels, no user preferences, and debugging is guesswork.


Expo Push Notifications

If you're building with Expo, you have a simpler option. Expo Push Notifications wraps FCM and APNs, eliminating most of the platform-specific configuration.

Why Teams Choose Expo

Expo's managed workflow handles the painful parts of push setup:

âś… No certificate management. Expo handles APNs credentials and FCM configuration automatically.

âś… Unified token format. Instead of separate FCM and APNs tokens, you get a single Expo push token.

âś… Works out of the box. In Expo-managed projects, push notifications require minimal setup.

For teams prioritizing speed to market, Expo removes significant friction.

Setting Up Expo Push

Install the required packages:

Copied!

npx expo install expo-notifications expo-device expo-constants

Register for push notifications and get the Expo push token:

Copied!

import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
async function registerForPushNotifications() {
if (!Device.isDevice) {
console.log('Push notifications require a physical device');
return null;
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.log('Permission not granted for push notifications');
return null;
}
const token = await Notifications.getExpoPushTokenAsync({
projectId: Constants.expoConfig?.extra?.eas?.projectId,
});
return token.data; // "ExponentPushToken[xxxxxx]"
}

Configure how notifications appear when the app is in the foreground:

Copied!

Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});

Handle notification interactions:

Copied!

import { useEffect, useRef } from 'react';
import * as Notifications from 'expo-notifications';
function useNotificationHandlers(navigation) {
const notificationListener = useRef();
const responseListener = useRef();
useEffect(() => {
// Notification received while app is foregrounded
notificationListener.current = Notifications.addNotificationReceivedListener(
notification => {
console.log('Notification received:', notification);
}
);
// User tapped on notification
responseListener.current = Notifications.addNotificationResponseReceivedListener(
response => {
const data = response.notification.request.content.data;
handleDeepLink(data, navigation);
}
);
return () => {
Notifications.removeNotificationSubscription(notificationListener.current);
Notifications.removeNotificationSubscription(responseListener.current);
};
}, [navigation]);
}

Sending Notifications via Expo's Service

Expo provides a push service you can call from your backend:

Copied!

// Backend: sending via Expo's push service
await fetch('https://exp.host/--/api/v2/push/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: 'ExponentPushToken[xxxxxx]',
title: 'Order Shipped',
body: 'Your order #456 is on the way!',
data: { orderId: '456', screen: 'OrderDetails' },
}),
});

Where Expo Push Hits Limits

Expo simplifies the basics, but the same production challenges apply:

⚠️ Single channel only. Expo Push is just push—email, SMS, and in-app require separate integrations.

⚠️ Limited delivery visibility. You know Expo accepted the message, but not whether the user received it.

⚠️ No user preferences. Opt-out logic and channel preferences are your responsibility.

⚠️ Expo-specific tokens. If you eject or move to bare workflow, you'll need to migrate to FCM tokens.

For MVPs and early-stage products, Expo Push is a great starting point. As you scale, an orchestration layer handles the complexity Expo doesn't address.

Using Courier with Expo

Courier supports Expo as a push provider. After getting your Expo push token, manually sync it to Courier:

Copied!

import Courier, { CourierPushProvider } from '@trycourier/courier-react-native';
// First, sign in with a JWT from your backend
const userId = 'user_123';
const jwt = await YourBackend.generateCourierJWT(userId);
await Courier.shared.signIn({ userId, accessToken: jwt });
// Then sync the Expo push token
await Courier.shared.setTokenForProvider({
provider: CourierPushProvider.EXPO,
token: 'ExponentPushToken[xxxxxx]'
});
// Now send via Courier - handles Expo push + email + SMS
// (from your backend)
await courier.send({
message: {
to: { user_id: 'user_123' },
template: 'order-shipped',
data: { orderNumber: 'ORD-456' }
}
});

This gives you Expo's managed workflow simplicity with Courier's multi-channel orchestration.


Why You Need a Notification Orchestration Layer

Direct FCM handles the last mile—getting a message from Google's servers to a device. It doesn't address the operational complexity that emerges at scale:

🔑 Token lifecycle. Tokens expire, become invalid, or belong to logged-out users. Without proper handling, you're sending into the void.

📬 Multi-channel coordination. Users miss push for many reasons: disabled permissions, do-not-disturb, app uninstalled. Email or SMS fallbacks require separate infrastructure.

⚙️ User preferences. Users want control—order updates via push, marketing via email, nothing after 10pm. Building this from scratch takes weeks.

🔍 Delivery observability. FCM confirms acceptance, not delivery. When users report missing notifications, you need visibility into what happened.

✏️ Template management. Copy changes going through engineering creates bottlenecks.

Courier sits between your application and delivery providers like FCM, APNs, SendGrid, and Twilio. One API call; Courier handles routing, preferences, retries, and tracking.

Copied!

import { CourierClient } from "@trycourier/courier";
const courier = CourierClient({
authorizationToken: process.env.COURIER_AUTH_TOKEN
});
// One call handles push, email, SMS based on user preferences
await courier.send({
message: {
to: { user_id: "user_123" },
template: "order-shipped",
data: {
orderNumber: "ORD-456",
trackingUrl: "https://tracking.example.com/ORD-456",
estimatedDelivery: "January 10, 2026"
}
}
});

What You Can Build with Courier and React Native

The Courier React Native SDK extends well beyond push. Here's what becomes possible:

In-App Notification Inbox

Give users a persistent home for notifications they can reference, mark as read, or act on later:

Copied!

npm install @trycourier/courier-react-native

Copied!

import Courier from '@trycourier/courier-react-native';
import { CourierInboxView } from '@trycourier/courier-react-native';
function NotificationInbox() {
return (
<CourierInboxView
onClickInboxMessageAtIndex={(message, index) => {
console.log(message);
// Toggle read state
message.read
? Courier.shared.unreadMessage({ messageId: message.messageId })
: Courier.shared.readMessage({ messageId: message.messageId });
}}
onClickInboxActionForMessageAtIndex={(action, message, index) => {
console.log(action);
}}
style={{ flex: 1 }}
/>
);
}

The component handles real-time updates, read/unread state, pagination, and styling customization.

User Preference Center

Let users control their experience without building preference UI from scratch:

Copied!

import { CourierPreferencesView } from '@trycourier/courier-react-native';
// Topic-based preferences
function NotificationPreferences() {
return (
<CourierPreferencesView
mode={{ type: 'topic' }}
style={{ flex: 1 }}
/>
);
}
// Or channel-based preferences
function ChannelPreferences() {
return (
<CourierPreferencesView
mode={{
type: 'channels',
channels: ['push', 'sms', 'email']
}}
style={{ flex: 1 }}
/>
);
}

Users configure which categories they want and through which channels. Preferences automatically apply to all notifications.

Multi-Channel Routing

Send to multiple channels based on preferences:

Copied!

await courier.send({
message: {
to: { user_id: "user_123" },
template: "payment-received",
routing: {
method: "all",
channels: ["push", "email", "inbox"]
},
data: {
amount: "$500.00",
senderName: "Acme Corp"
}
}
});

Or create escalation chains—push first, SMS if undelivered, then email:

Copied!

await courier.send({
message: {
to: { user_id: "user_123" },
template: "urgent-alert",
routing: {
method: "single",
channels: ["push", "sms", "email"]
}
}
});

Automatic Token Management

The SDK handles the token lifecycle that causes issues with direct FCM. First, generate a JWT on your backend, then sign in:

Copied!

import Courier from '@trycourier/courier-react-native';
// Generate JWT on your backend, then sign in
const userId = 'user_123';
const jwt = await YourBackend.generateCourierJWT(userId);
await Courier.shared.signIn({
userId: userId,
accessToken: jwt
});
// Courier automatically:
// - Registers push tokens (APNS on iOS, FCM on Android)
// - Handles token refresh
// - Associates tokens with user profile
// - Cleans up on signOut
// When user logs out
await Courier.shared.signOut();

Users stay signed in between app sessions. The SDK persists credentials automatically.

You can also listen for push notification events:

Copied!

import Courier from '@trycourier/courier-react-native';
// Configure iOS foreground presentation
Courier.setIOSForegroundPresentationOptions({
options: ['sound', 'badge', 'list', 'banner']
});
// Listen for push events
const pushListener = Courier.shared.addPushListener({
onPushClicked: (push) => {
console.log('Push clicked:', push);
},
onPushDelivered: (push) => {
console.log('Push delivered:', push);
},
});
// Clean up when done
pushListener.remove();

👉 Learn more about Courier's mobile notification capabilities.


Comparing Push Notification Approaches

CapabilityCourierFCM DirectExpo PushOneSignal
Basic push notificationsâś…âś…âś…âś…
Setup complexityLowHighLowMedium
Certificate managementManagedManualAutomaticSemi-managed
Token managementManagedBuild yourselfSimpler tokensManaged
Delivery analyticsComprehensiveBasicBasicGood
Multi-channel (email, SMS)Unified APISeparate integrationSeparate integrationLimited
In-app inboxNative SDK componentBuild yourselfBuild yourselfLimited
User preferencesFull preference centerBuild yourselfBuild yourselfBasic opt-out
Channel fallbacksAutomatic routingBuild yourselfBuild yourselfManual
Template managementVisual designer + APICode changesCode changesDashboard
Non-engineer editing✅❌❌Limited
Works with Expo managedâś…Via configâś… Nativeâś…

When to Use Each Approach

🏆 Courier is the production path for apps needing multi-channel notifications, user preferences, delivery tracking, and team collaboration—works with both Expo and bare React Native.

🛠️ FCM Direct makes sense for bare React Native projects with simple single-channel needs and engineering bandwidth for platform configuration.

⚡ Expo Push is ideal for Expo-managed projects that need push notifications quickly without dealing with certificates and FCM setup.

📊 OneSignal works for push-focused apps needing better analytics than raw FCM/Expo, but not multi-channel orchestration.

Courier doesn't replace FCM or Expo—it orchestrates them alongside your other channels, handling complexity between "send notification" and "user received it."


Start Building with Courier

Whether you're implementing your first push notification or adding multi-channel orchestration to an existing app, Courier provides infrastructure that grows with you.

Free tier includes: push, email, SMS, and in-app notifications; React Native SDK with inbox and preference components; visual template designer; delivery tracking.

👉 Create your free Courier account →

👉 Courier React Native SDK on GitHub →

👉 Mobile notifications with Courier →


Frequently Asked Questions

How do I send push notifications in React Native?

Use Firebase Cloud Messaging with @react-native-firebase/messaging. Configure your Firebase project, request iOS permissions, register device tokens, and set up handlers for foreground/background notifications. For production, add an orchestration layer like Courier to handle token management, delivery tracking, and multi-channel routing.

What's the difference between FCM and APNs for React Native?

FCM works on both Android and iOS—for iOS, it routes through APNs behind the scenes. Configure FCM once and it handles both platforms, though iOS still requires APNs certificates. With Courier as your orchestration layer, you don't interact with FCM or APNs directly.

Why aren't my React Native push notifications showing on iOS?

Common causes: APNs certificates misconfigured, permissions not requested or denied, app in foreground (iOS doesn't show system notifications by default), or invalid device token. Check that messaging().requestPermission() returns AUTHORIZED. Courier's delivery logs show exactly where notifications fail.

How do I add an in-app notification inbox to React Native?

Building from scratch requires a backend, API, WebSocket for real-time updates, read/unread state, and UI components. Courier's React Native SDK provides a pre-built CourierInboxView—drop it in and notifications sent through Courier automatically appear.

How do I let users manage notification preferences in React Native?

Courier provides a CourierPreferencesView component that renders complete preference UI. Users control which categories they receive and through which channels. Preferences automatically apply to all notifications—no backend work required.

Can I send push notifications with email/SMS fallbacks?

Not with FCM alone. You need an orchestration layer. Courier's routing lets you send to multiple channels simultaneously or create escalation chains—push first, SMS if undelivered, then email. One API call handles the flow.

What's the best push notification service for React Native in 2026?

For basic push, FCM is free and sufficient. For production apps needing multi-channel notifications, user preferences, and delivery tracking, Courier provides the most complete solution—orchestrating FCM alongside email, SMS, and in-app through a single API.

How do I test push notifications during React Native development?

Push doesn't work in iOS Simulator—use a physical device. Android Emulator works with FCM. Courier's test mode sends to specific devices without affecting production, and delivery logs show exactly what happened.

Can I use Courier with Expo managed workflow?

Yes. Courier supports Expo push tokens as a provider. After signing in with a JWT, use Courier.shared.setTokenForProvider({ provider: CourierPushProvider.EXPO, token: '...' }) to sync your Expo push token. Courier then handles delivery alongside email, SMS, and in-app channels. You get Expo's simple setup with Courier's multi-channel orchestration.

Should I use Expo Push or FCM for React Native?

If you're using Expo's managed workflow, start with Expo Push—it's simpler and handles certificate management automatically. If you're in a bare React Native project or need more control, FCM is the standard. Either way, adding Courier as an orchestration layer gives you multi-channel support, preferences, and delivery tracking regardless of which push provider you use.


Explore the Courier React Native SDK and mobile notification capabilities for implementation details. 🚀

Similar resources

Twilio Integration, SendGrid Integration
GuideIntegrationsProduct Management

Twilio Integrations with Courier: SMS, SendGrid, Segment

Twilio owns critical notification infrastructure: SMS for billions of messages, SendGrid for email at scale, and Segment for customer data aggregation. Using them together means maintaining three APIs, three credential sets, and zero coordination between channels. Courier solves this by providing a single integration point for all three Twilio products. Connect your accounts, use one API to send across SMS and email, trigger notifications from Segment events, and orchestrate multi-channel delivery with routing rules and failover built in.

By Kyle Seyler

December 10, 2025

StreamChat
IntegrationsNotifications Landscape

Courier + Stream: The Future of Customer Engagement is Here

Modern apps need more than features—they need conversations and intelligent communication. Courier's omnichannel notification platform combined with Stream's real-time messaging infrastructure transforms how developers build engaging experiences. Send notifications across email, SMS, push, and in-app channels while powering real-time chat, video calls, and activity feeds. With Courier's new MCP server, implement Stream directly from your IDE using AI agents. From indie developers to enterprises, build production-ready communication features in days.

By Kyle Seyler

October 29, 2025

notification center twilio
GuideIntegrations

How to Build a Notification Center for Web & Mobile Apps

Building a notification center from scratch takes 3-6 months. This comprehensive guide shows developers how to implement a production-ready notification center with multi-channel support in days using React, React Native, iOS, Android, Flutter, or JavaScript. Learn how to add in-app notifications, toast alerts, push notifications, email, and SMS with automatic cross-channel state synchronization. Compare building custom vs. using platforms like Courier, Novu, and OneSignal. Includes real code examples and best practices.

By Kyle Seyler

October 17, 2025

Multichannel Notifications Platform for SaaS

Products

Platform

Integrations

Customers

Blog

API Status

Subprocessors


© 2026 Courier. All rights reserved.