Push Notifications with Expo: From Setup to Debugging Delivery
React Native push notifications with Expo: credentials, permissions, Android channels, tokens, sending, taps and debugging notifications that never arrive.
Full-Stack, Mobile & AI Engineer
Push notifications are simple to send and surprisingly hard to make reliable. The first test notification arrives and everyone celebrates. Then some Android users never get them, iOS users who said "no" can't be asked again, tokens expire silently, and a notification tap opens the wrong screen. Most of those problems come from setup details that are easy to skip.
This guide covers React Native push notifications with Expo end to end in 2026: credentials, permissions and Android channels, getting and storing tokens, sending from your server, handling taps, and a checklist for debugging notifications that don't arrive.
Key takeaways
- Use a development build to test remote push. Since SDK 53, Expo Go on Android doesn't support remote notifications.
- Create an Android notification channel before requesting the token; on Android 13 the permission prompt won't appear until a channel exists.
- Ask for permission at a meaningful moment, not on first launch, because an iOS "Don't Allow" is hard to undo.
- Store tokens per device on your server, update them on every launch, and remove them when the push service reports them invalid.
- Check delivery receipts and handle errors; "sent" doesn't mean "delivered".
How the pieces fit
- Credentials: an APNs key for iOS and Firebase Cloud Messaging (FCM v1) credentials for Android, uploaded to EAS if you use Expo's push service.
- Permission: the user allows notifications.
- Token: the app gets a push token for this device and sends it to your server.
- Send: your server sends a message to that token through Expo's push service (or directly to APNs and FCM).
- Receive: the OS shows the notification; tapping it opens the app, ideally on the right screen.
Permissions, channels and tokens
import * as Notifications from "expo-notifications";
import Constants from "expo-constants";
import { Platform } from "react-native";
export async function registerForPush(): Promise<string | null> {
if (Platform.OS === "android") {
await Notifications.setNotificationChannelAsync("default", {
name: "General",
importance: Notifications.AndroidImportance.DEFAULT,
});
}
const { status } = await Notifications.requestPermissionsAsync();
if (status !== "granted") return null;
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
const { data: token } = await Notifications.getExpoPushTokenAsync({ projectId });
return token; // send to your server with the user ID and device info
}Create channels for each kind of notification your app sends (for example reminders, messages, promotions) so Android users can control them individually. Wrap the token request in error handling and retry later if the device is offline.
Asking at the right moment
iOS shows the permission prompt once; after a refusal, users must go to Settings. So:
- Ask when the user does something that benefits from notifications ("Remind me before class").
- Show your own explanation screen first, then the system prompt.
- Consider provisional authorisation on iOS for quiet, low-stakes notifications that appear in the notification centre without a prompt.
Foreground behaviour and taps
Decide what happens when a notification arrives while the app is open:
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: false,
shouldSetBadge: false,
}),
});(The older shouldShowAlert option is deprecated in favour of shouldShowBanner and shouldShowList.) For taps, include a route or URL in the notification's data and navigate there when the user opens it, including when the tap launched the app from closed:
useEffect(() => {
const sub = Notifications.addNotificationResponseReceivedListener((response) => {
const url = response.notification.request.content.data?.url;
if (typeof url === "string") router.push(url); // e.g. "/bookings/812"
});
return () => sub.remove();
}, []);Check Notifications.getLastNotificationResponseAsync() at startup too, so a tap that cold-started the app still lands on the right screen.
Images, icons and styling
- Android small icon: Android draws the status-bar icon as a single colour, so a full-colour logo shows as a grey square. Provide a white, transparent-background icon and an accent colour through the
expo-notificationsconfig plugin (iconandcolor). - Images in notifications: Android can show a large image from the payload. iOS needs a Notification Service Extension that downloads the image and attaches it before display, which in Expo means adding the extension through a config plugin.
- Actions and categories: buttons such as "Reply" or "Mark as done" are defined as notification categories in the app, then referenced by the payload.
Expo's push service, Firebase directly, or no Firebase at all?
Expo's push service is a convenience layer: you send to an Expo push token, and Expo forwards to Apple's APNs and Google's FCM. You can skip it and talk to APNs and FCM yourself (for example with @react-native-firebase/messaging and the native device token), which suits teams that already run FCM for web or want one less hop.
"Without Firebase" is only possible on iOS, by sending to APNs directly. On Android, remote push to devices with Google Play services goes through FCM whichever library you use, so you'll need a Firebase project for Android even if you never use Firebase otherwise. Libraries such as Notifee focus on displaying and customising notifications (channels, styles, actions) and are usually paired with FCM for delivery; for most apps, expo-notifications covers both.
Sending from your server
With Expo's push service, your server sends messages to Expo push tokens in batches. Two habits keep delivery healthy:
- Read push tickets and receipts. Receipts tell you whether Apple or Google accepted the message, and errors such as
DeviceNotRegisteredmean you should delete that token. - Respect user preferences and quiet hours, and don't send marketing pushes without explicit opt-in (Apple's guideline 4.5.4 requires consent and an in-app way to opt out).
From the field: Push notifications through APNs and FCM are a standard part of the apps I ship, from push-driven merchandising in Bartesian's commerce app to notifications in Honeydew's dating app. The setup is the easy part; deliverability comes from token hygiene, receipts and asking for permission when it makes sense to the user.
Notifications people don't turn off
Deliverability isn't only technical. Every irrelevant notification teaches users to disable them, and once they do, you lose the channel for the messages that matter. A few rules help:
- Make every notification useful on its own: "Your 7:00 class starts in an hour" beats "Don't forget to check the app!"
- Personalise timing: respect time zones and quiet hours, and avoid sending several notifications in a row.
- Separate transactional from marketing with different channels, so users can mute promotions without missing bookings or messages.
- Measure opt-outs and disabled channels, not just open rates, and treat rising opt-outs as a product problem.
Debugging notifications that don't arrive
| Check | What to look for |
|---|---|
| Build type | Testing remote push in Expo Go on Android won't work since SDK 53; use a development build |
| Credentials | APNs key and FCM v1 credentials uploaded and matching the app's bundle ID and package |
| Permission | User granted notifications; on Android 13+ the channel existed before the request |
| Token | Current token stored on the server for this device and user |
| Receipts | Errors such as DeviceNotRegistered or credential problems |
| Device state | Focus modes, battery saver, or the user disabled a channel |
| Payload | Correct project, channel ID and data fields; not exceeding size limits |
Test on real devices, and send a test notification from your own server code path, not only from a dashboard tool. A notification that works from a dashboard but not from your backend usually points to the wrong token, project or payload format.
FAQ
How do I set up push notifications in Expo?
Install expo-notifications, configure your APNs key and FCM v1 credentials with EAS, create an Android notification channel, request permission, get the Expo push token with your EAS project ID, store it on your server, and send messages through Expo's push service. Test with a development build on real devices.
Why don't push notifications work in Expo Go?
Since SDK 53, Expo Go on Android doesn't support remote push notifications, and in general push requires your app's own credentials and identifiers. Use a development build created with EAS to test remote notifications. Local notifications still work in Expo Go.
Why are my Android notifications not showing?
Common causes are a missing notification channel, permission not granted on Android 13 or later, a user-disabled channel, battery optimisation, or invalid FCM credentials. Create the channel before requesting the token and check push receipts for errors.
Should I use Expo's push service or FCM directly?
Expo's push service is simpler: one API and token format for both platforms, with receipts. Sending directly through FCM and APNs gives more control and avoids an extra hop, which some larger apps prefer. Both use the same platform credentials underneath.
Next steps
If your app's notifications are unreliable, or you're adding them for the first time, I can set up credentials, permission flows, server-side sending and receipts so they arrive when they should. Get in touch, or see how I work as a React Native developer.
Sources
- Notifications (expo-notifications), Expo
- Push notifications setup, Expo
- Sending notifications with Expo's push service, Expo
- Notification runtime permission (Android 13), Android Developers
- App Review Guidelines (4.5.4), Apple
Related
Deep Links and Universal Links with Expo Router
Deep linking in React Native with Expo Router: URL schemes, iOS universal links, Android App Links, testing, deferred links after Firebase Dynamic Links.
Background Tasks in React Native: What Actually Runs
What really runs in the background in React Native apps in 2026: iOS and Android limits, expo-background-task, location and BLE tasks, silent push.
HealthKit and Health Connect in React Native
Integrate Apple HealthKit and Android Health Connect in React Native: libraries, permissions, reading and writing data, privacy rules and Google Fit's end.
