Kyle Seyler
January 07, 2026

Table of contents
Table of Contents
Why React Native for Mobile Notifications
Implementing Push Notifications in React Native
Expo Push Notifications
Why You Need a Notification Orchestration Layer
What You Can Build with Courier and React Native
Comparing Push Notification Approaches
Start Building with Courier
Frequently Asked Questions
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.
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.
Let's implement push notifications from scratch, then show how an orchestration layer simplifies the production path.
FCM is the standard foundation—it handles Android natively and routes to APNs for iOS.
Copied!
yarn add @react-native-firebase/appyarn add @react-native-firebase/messaging# iOS: install podscd ios/ && pod install
You'll need platform-specific configuration:
google-services.json to your projectGoogleService-Info.plistiOS 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;}
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]);}
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 foregroundconst unsubscribeForeground = messaging().onMessage(async message => {showInAppNotification(message);});// App in background - user tapped notificationconst unsubscribeBackground = messaging().onNotificationOpenedApp(message => {handleNotificationNavigation(message, navigation);});// App was closed - opened via notificationmessaging().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.
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.
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.
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 foregroundednotificationListener.current = Notifications.addNotificationReceivedListener(notification => {console.log('Notification received:', notification);});// User tapped on notificationresponseListener.current = Notifications.addNotificationResponseReceivedListener(response => {const data = response.notification.request.content.data;handleDeepLink(data, navigation);});return () => {Notifications.removeNotificationSubscription(notificationListener.current);Notifications.removeNotificationSubscription(responseListener.current);};}, [navigation]);}
Expo provides a push service you can call from your backend:
Copied!
// Backend: sending via Expo's push serviceawait 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' },}),});
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.
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 backendconst userId = 'user_123';const jwt = await YourBackend.generateCourierJWT(userId);await Courier.shared.signIn({ userId, accessToken: jwt });// Then sync the Expo push tokenawait 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.
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 preferencesawait 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"}}});
The Courier React Native SDK extends well beyond push. Here's what becomes possible:
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 (<CourierInboxViewonClickInboxMessageAtIndex={(message, index) => {console.log(message);// Toggle read statemessage.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.
Let users control their experience without building preference UI from scratch:
Copied!
import { CourierPreferencesView } from '@trycourier/courier-react-native';// Topic-based preferencesfunction NotificationPreferences() {return (<CourierPreferencesViewmode={{ type: 'topic' }}style={{ flex: 1 }}/>);}// Or channel-based preferencesfunction ChannelPreferences() {return (<CourierPreferencesViewmode={{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.
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"]}}});
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 inconst 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 outawait 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 presentationCourier.setIOSForegroundPresentationOptions({options: ['sound', 'badge', 'list', 'banner']});// Listen for push eventsconst pushListener = Courier.shared.addPushListener({onPushClicked: (push) => {console.log('Push clicked:', push);},onPushDelivered: (push) => {console.log('Push delivered:', push);},});// Clean up when donepushListener.remove();
👉 Learn more about Courier's mobile notification capabilities.
| Capability | Courier | FCM Direct | Expo Push | OneSignal |
|---|---|---|---|---|
| Basic push notifications | âś… | âś… | âś… | âś… |
| Setup complexity | Low | High | Low | Medium |
| Certificate management | Managed | Manual | Automatic | Semi-managed |
| Token management | Managed | Build yourself | Simpler tokens | Managed |
| Delivery analytics | Comprehensive | Basic | Basic | Good |
| Multi-channel (email, SMS) | Unified API | Separate integration | Separate integration | Limited |
| In-app inbox | Native SDK component | Build yourself | Build yourself | Limited |
| User preferences | Full preference center | Build yourself | Build yourself | Basic opt-out |
| Channel fallbacks | Automatic routing | Build yourself | Build yourself | Manual |
| Template management | Visual designer + API | Code changes | Code changes | Dashboard |
| Non-engineer editing | ✅ | ❌ | ❌ | Limited |
| Works with Expo managed | âś… | Via config | âś… Native | âś… |
🏆 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."
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 →
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.
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.
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.
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.
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.
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.
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.
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.
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.
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. 🚀

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

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

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
© 2026 Courier. All rights reserved.