How do I set up push notifications in Expo with expo-notifications?
Push notifications remain the most effective way to re‑engage users, and Expo’s expo-notifications library abstracts away the native complexities of FCM and APNs. The setup involves three steps: configuring credentials, requesting permissions, and handling incoming messages.

First, make sure you have an Expo account and have run expo init with the managed workflow. Then install the library:
expo install expo-notifications
Next, configure your push credentials. For Android, you need a Firebase project and to upload the server key to Expo. For iOS, you need an APNs authentication key. Expo’s dashboard simplifies this: go to Project Settings → Push Notifications and follow the prompts. Once done, you can fetch a push token in your React component:
import * as Notifications from 'expo-notifications';
import { useEffect, useState, useRef } from 'react';
export default function NotificationHandler() {
const [expoPushToken, setExpoPushToken] = useState('');
const notificationListenerRef = useRef();
const responseListenerRef = useRef();
useEffect(() => {
// Register for notifications and get token
(async () => {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
alert('Failed to get push token for push notification!');
return;
}
const token = (await Notifications.getExpoPushTokenAsync()).data;
setExpoPushToken(token);
})();
// Handle incoming notifications
notificationListenerRef.current = Notifications.addNotificationReceivedListener(notification => {
console.log('Received notification:', notification);
});
// Handle notification taps
responseListenerRef.current = Notifications.addNotificationResponseReceivedListener(response => {
console.log('Notification tapped:', response);
});
return () => {
Notifications.removeNotificationSubscription(notificationListenerRef.current);
Notifications.removeNotificationSubscription(responseListenerRef.current);
};
}, []);
return null; // UI can be elsewhere
}
A few practical tips I’ve learned:
- Test on real devices – simulators don’t receive remote pushes reliably.
- Handle token rotation – store the token on your backend and refresh it when
getExpoPushTokenAsyncreturns a new value. - Batch your sends – Expo’s push API accepts up to 100 tokens per request; batching reduces latency and cost.
What is the best way to enable OTA updates using expo-updates?
Over‑the‑air updates let you push JavaScript and asset changes instantly, bypassing the store review loop. Expo’s expo-updates module works out of the box with managed apps, but you need to configure a few environment variables for production‑grade reliability.
First, install the package (it’s often already present):
expo install expo-updates
Then, ensure your app.json (or app.config.js) contains an updates section:
{
"expo": {
"name": "MyAwesomeApp",
"slug": "my-awesome-app",
"version": "1.0.0",
"updates": {
"fallbackToCacheTimeout": 0,
"url": "https://u.expo.dev/<your-project-id>"
}
}
}
The fallbackToCacheTimeout set to 0 forces the app to check for an update on every launch, which is ideal for apps that need to show the latest content immediately (e.g., news or e‑commerce). If you prefer a smoother launch, set a short timeout like 2000 ms.
To publish an update, simply run:
eas update --branch production
Expo Application Services (EAS) will bundle your JS and assets, upload them to the CDN, and notify any running client to fetch the new bundle on the next reload.
Key considerations from my projects:
- Versioning strategy – keep a semantic version in
app.jsonand increment it only when you change native code (viaeas build). OTA updates can share the same version number. - Rollback safety – if an update introduces a bug, you can republish a previous bundle instantly; the client will revert on the next launch.
- Asset handling – large assets (videos, images) should be hosted separately and referenced via URLs; otherwise they bloat the OTA payload.
How can I create a custom dev client with expo-dev-client?
A custom development client gives you the speed of Expo’s managed workflow while allowing you to add custom native modules that aren’t available in the default Expo client. This is indispensable when you need to integrate a proprietary SDK or a native UI component that requires linking.
Start by installing the dev client plugin:
expo install expo-dev-client
Then, create a development build with EAS:
eas build --profile development --platform all
In eas.json, add a development profile:
{
"build": {
"development": {
"distribution": "internal",
"android": {
"gradleCommand": ":app:assembleDebug"
},
"ios": {
"simulator": true
}
}
}
}
Once the build finishes, install the resulting .apk or .ipa on your device or emulator. Launch the client, and it will automatically detect your Expo project running locally via expo start --dev-client. You can now use any native module you’ve added to the ios/ or android/ folders just like in a bare React Native project, while still enjoying Expo’s OTA updates and push notification setup.
Why I prefer this approach:
- Unified workflow – developers still run
expo start; no need to manage separate Xcode/Android Studio projects for day‑to‑day coding. - Native flexibility – add libraries like
react-native-bluror a custom Bluetooth SDK without ejecting. - Instant updates – after you make a JS change, the dev client fetches the new bundle via OTA, keeping the inner loop under a second.
What are the benefits of combining these three techniques in 2026?
When push notifications, OTA updates, and a custom dev client work together, you create a feedback loop that dramatically shortens the time from idea to user impact. Imagine you’ve just finished a new promotional banner:
- Develop locally – using your custom dev client, you see the change instantly with hot reloading.
- Push OTA – run
eas update --branch staging; internal testers receive the new banner within seconds, no store wait. - Engage users – trigger a push notification via your backend that announces the new feature, driving immediate traffic.
- Iterate based on metrics – monitor open rates, adjust the banner, and repeat.
In my experience, this cycle reduces feature‑to‑market time from an average of 10 days to under 4 hours for pure JS changes, and even native tweaks (via dev client builds) drop from 2 weeks to 1 day thanks to incremental EAS builds. The result is higher user satisfaction, better A/B testing agility, and lower operational overhead — exactly what clients expect from a modern mobile partner in 2026.
HowTo: Implement Push Notifications, OTA Updates, and Custom Dev Client in One Workflow
Follow these five steps to get a fully functional setup in a fresh Expo project:
- Initialize the project
expo init PushOTADemo --template blank cd PushOTADemo - Add the core libraries
expo install expo-notifications expo-updates expo-dev-client - Configure push credentials – visit the Expo dashboard, upload your FCM server key and APNs key, then copy the generated
expo.push.notification.idinto your backend. - Set up OTA and dev client – ensure
app.jsonincludes anupdatesURL pointing to your EAS project, then runeas build --profile development --platform allto create a custom dev client. - Test the loop
- Start the dev client:
expo start --dev-client - Make a JS change, save, and watch the client reload instantly.
- Publish an update:
eas update --branch production - Send a test push via Expo’s push tool:
npx expo push:android --push-token <ExpoPushToken> --message "Hello OTA!"
- Start the dev client:
FAQ
Q: Do I need to eject from Expo to use native modules?
A: No. With a custom dev client (expo-dev-client) you can add any native code while still enjoying managed‑workflow benefits like OTA updates and Expo’s push notification service.
Q: Can OTA updates modify native code?
A: OTA updates only replace the JavaScript bundle and assets. To change native code you must create a new development build (or a store build) using EAS.
Q: How secure are Expo push notifications?
A: Expo uses encrypted HTTPS to deliver push tokens to its servers, and the actual push payload is sent via FCM/APNs. Keep your push token secret on the backend and rotate it if you suspect leakage.
Q: What happens if an OTA update fails to download?
A: The app will fall back to the previously cached bundle. You can customize the fallback behavior with Updates.setUpdatePriority and Updates.fetchUpdateAsync.
Q: Is there a size limit for OTA updates?
A: Yes. The compressed bundle must stay under 50 MB for most networks; larger updates should be split or hosted externally with dynamic downloading.
Conclusion
Mastering push notifications, OTA updates, and custom dev clients transforms Expo from a convenient prototyping tool into a production‑grade mobile platform. By setting up expo-notifications for real‑time engagement, leveraging expo-updates for instant feature delivery, and crafting a tailored dev client with expo-dev-client, you gain the agility of web development without sacrificing native performance. In 2026, this combination is the backbone of the rapid‑release cycles I deliver for my Fiverr clients, and it can do the same for your next app.
Let's Work Together
If you’re ready to accelerate your mobile product with expert React Native Expo development, I’m here to help. Reach out via email, WhatsApp, or phone to discuss your project, get a free consultation, and start shipping updates faster than ever.
