Expo Push Notifications: Complete Setup Guide for React Native (2026)
Set up push notifications in an Expo React Native app, from permission prompts and ExpoPushToken registration to Android FCM v1, iOS credentials, backend sends, receipts, and production debugging.
The short version
Install expo-notifications and expo-constants, add the expo-notifications config plugin, request permission, get an ExpoPushToken with your EAS project ID, store that token on your backend, then send notifications through Expo Push Service.
Expo makes push notifications much less painful because your app can talk to one Expo-facing API while Expo handles the handoff to Firebase Cloud Messaging on Android and Apple Push Notification service on iOS. You still need the native credentials, a real build, and a backend that treats push tokens as user data.
1. Know what you need first
Push notifications touch JavaScript, native credentials, app permissions, and your server. Before writing code, make sure you have:
- An Expo React Native app with a stable Android package name and iOS bundle identifier.
- An EAS project, because the push token should be tied to the EAS
projectId. - A development build or store build for realistic testing.
- A physical iOS or Android device for the most reliable test path.
- A backend or database where you can store push tokens by user or installation.
Android emulators with Google Play services can receive push notifications. iOS Simulator support exists on modern macOS and Xcode versions, but I still treat a physical iPhone as the final answer before shipping.
2. Install the Expo libraries
Install through Expo so the package versions line up with your SDK.
npx expo install expo-notifications expo-constantsexpo-notifications handles permission requests, notification listeners, Android channels, and push token generation. expo-constants gives you access to the EAS project ID from app config at runtime.
3. Add the config plugin
Add expo-notifications to the plugin list in app.json or app.config.js. This lets Expo configure the native notification pieces when you build the app.
{
"expo": {
"name": "My App",
"slug": "my-app",
"plugins": [
"expo-notifications"
]
}
}If you already have plugins, keep them and add expo-notifications to the same array. After plugin changes, create a new native build; a Metro reload cannot add native notification configuration to an installed binary.
4. Read the EAS project ID
When you request an Expo push token, pass the EAS project ID. That connects the token to the right Expo project even if the account or project ownership changes later.
import Constants from "expo-constants";
export function getProjectId() {
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) {
throw new Error("EAS project ID is missing.");
}
return projectId;
}If this throws in development, run eas init or check that your app config has an extra.eas.projectId value.
5. Register for push notifications
Create one small registration function. It should set the Android channel first, ask for permission, then request the Expo push token.
import { Platform } from "react-native";
import * as Notifications from "expo-notifications";
import { getProjectId } from "./getProjectId";
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
export async function registerForPushNotificationsAsync() {
if (Platform.OS === "android") {
await Notifications.setNotificationChannelAsync("default", {
name: "default",
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: "#5eead4",
});
}
const existingPermission = await Notifications.getPermissionsAsync();
let finalStatus = existingPermission.status;
if (finalStatus !== "granted") {
const requestedPermission = await Notifications.requestPermissionsAsync();
finalStatus = requestedPermission.status;
}
if (finalStatus !== "granted") {
return null;
}
const token = await Notifications.getExpoPushTokenAsync({
projectId: getProjectId(),
});
return token.data;
}Call this after the user understands why notifications are useful. A cold permission prompt on first launch is easy to decline and hard to recover from.
6. Store the token on your backend
An Expo push token identifies an app installation. Store it with enough context to send responsibly: user ID, device ID if you have one, platform, app version, and the timestamp when it was last seen.
import { useEffect } from "react";
import { Platform } from "react-native";
import { registerForPushNotificationsAsync } from "./notifications";
export function useRegisterPushToken(userId: string | null) {
useEffect(() => {
if (!userId) {
return;
}
async function register() {
const token = await registerForPushNotificationsAsync();
if (!token) {
return;
}
await fetch("https://api.example.com/push-tokens", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
userId,
token,
platform: Platform.OS,
}),
});
}
register();
}, [userId]);
}Do not store just one token per user unless your product truly supports one device only. Most real users have reinstalls, new phones, tablets, or both iOS and Android devices.
7. Set up Android FCM v1 and iOS credentials
Expo Push Service still needs platform credentials behind the scenes. EAS Build is the easiest route because it stores notification credentials with the Expo project.
Android
For Android, configure Firebase Cloud Messaging v1 credentials for the app. The important parts are that your Firebase Android app uses the same package name as Expo, and the FCM credentials are uploaded for the correct EAS project.
iOS
For iOS, you need an Apple Developer account and APNs credentials. When EAS CLI asks whether to set up push notifications and generate an Apple Push Notifications service key, answer yes unless your team already manages APNs keys manually.
Credentials are project-specific
If notifications work in one Expo project but not another, check credentials before rewriting app code. Package names, bundle IDs, EAS project IDs, and credentials all have to point at the same app.
8. Build a development app
Once the plugin and credentials are ready, create a development build. This gives you a real native binary with notification support included.
npx eas-cli@latest login
npx eas-cli@latest build:configure
npx eas-cli@latest build --profile development --platform ios
npx eas-cli@latest build --profile development --platform androidInstall the build on your device, start Metro with npx expo start --dev-client, and confirm the app logs an ExponentPushToken. That token is what your backend sends to Expo Push Service.
9. Send from a backend
You can call the Expo Push Service directly with HTTPS. Your production backend should load target tokens from your database, send messages in batches, and record the ticket IDs for receipt checks.
import express from "express";
const app = express();
app.use(express.json());
app.post("/send-push", async (req, res) => {
const { token, title, body } = req.body;
if (!ExpoPushTokenPattern.test(token)) {
return res.status(400).json({ error: "Invalid Expo push token." });
}
const response = await fetch("https://exp.host/--/api/v2/push/send", {
method: "POST",
headers: {
Accept: "application/json",
"Accept-Encoding": "gzip, deflate",
"Content-Type": "application/json",
},
body: JSON.stringify({
to: token,
sound: "default",
title,
body,
data: { screen: "inbox" },
}),
});
const ticket = await response.json();
res.json(ticket);
});
const ExpoPushTokenPattern = /^ExponentPushToken\[[A-Za-z0-9_-]+\]$/;
app.listen(3000);For larger sends, use the Expo server SDK for your backend language. The Node SDK already limits concurrent connections, which helps avoid avoidable rate and network failures.
10. Handle foreground notifications and taps
There are two separate events to care about: a notification received while the app is open, and a user tapping a notification to open or resume the app.
import { useEffect, useRef } from "react";
import * as Notifications from "expo-notifications";
export function useNotificationListeners() {
const receivedListener = useRef<Notifications.Subscription | null>(null);
const responseListener = useRef<Notifications.Subscription | null>(null);
useEffect(() => {
receivedListener.current =
Notifications.addNotificationReceivedListener((notification) => {
console.log("Notification received", notification);
});
responseListener.current =
Notifications.addNotificationResponseReceivedListener((response) => {
const data = response.notification.request.content.data;
console.log("Notification tapped", data);
});
return () => {
receivedListener.current?.remove();
responseListener.current?.remove();
};
}, []);
}Use the tap response to route the user to the right screen, but keep the payload small. Put identifiers in data, then fetch the latest content after the app opens.
11. Check tickets and receipts
A successful send request means Expo accepted the notification for delivery, not that the device definitely displayed it. Store ticket IDs, then check receipts later for permanent failures.
- Retry temporary network,
429, and5xxfailures with exponential backoff. - Fix malformed payloads immediately; retrying the same bad payload will not help.
- Remove or disable tokens when receipts report that the device is not registered.
- Log enough context to debug: user ID, token ID, message type, ticket ID, receipt status, and error code.
This is the difference between a demo and a production notification system. Token cleanup keeps sends faster, cheaper, and less noisy over time.
12. Troubleshooting checklist
- No token: confirm permissions are granted, the EAS project ID exists, and the app is a development or production build with the plugin included.
- Android receives nothing: check FCM v1 credentials, package name, Google Play services, and notification channel setup.
- iOS receives nothing: check APNs credentials, bundle identifier, Apple Developer account access, and whether the device is registered for development builds.
- Foreground notification is silent: confirm
Notifications.setNotificationHandlerreturns the display and sound behavior you expect. - Backend send fails: validate the token format, payload size, credential errors, and Expo Push Service response body.
- Works on one build but not another: compare EAS project ID, bundle ID, package name, and credentials between the builds.
Final test plan
- Install a fresh development build on a real iOS or Android device.
- Trigger the permission prompt from a screen that explains the benefit.
- Confirm your backend receives and stores the Expo push token.
- Send one test notification from the Expo push notification tool.
- Send one test notification from your backend.
- Confirm foreground display, background delivery, and tap routing.
- Check push tickets and receipts, then remove invalid tokens.
Once those pass, you have the core loop: app registers, backend stores, backend sends, app receives, receipts clean up the leftovers.