Expo Notifications
Set up Expo Notifications for push notifications in an Expo app with ReadyNative: Expo push service + local notifications · dev build required for remote…
Expo Go: no - dev build required
Ships native code. Make a dev build with bunx expo run:ios / bunx expo run:android (or bunx eas-cli build --profile development).
This gives you remote and local push through Expo's push service: src/lib/push.ts implements the shim, src/hooks/use-push-token.ts is a provider-free store that checks permission and calls getExpoPushTokenAsync({ projectId }) with the id from Constants.expoConfig.extra.eas.projectId, and PushProvider (order 80) installs the foreground handler, creates the Android default channel, and deep-links to data.url when a notification is tapped - including from a cold start. If you set up with --with-examples there's also an example screen at /examples/push with a copyable token and a local test notification. Either way you get a bun run push:test script that sends one message through Expo's API. @/lib/push keeps the same API whichever option you pick - pick this when you want push without running your own APNs and FCM plumbing.
Setup
bun run setup --push expo-notifications- Run
bunx eas-cli init. It can't write into the dynamicapp.config.ts, so it only prints the project id: paste it intoreadynative.config.ts→app.easProjectId(or setEAS_PROJECT_ID), andapp.config.tsturns it intoextra.eas.projectId, which is what the token request needs. With an empty project id the hook reportsunsupportedand warns in the console. - In the [Apple Developer] portal, create a Push Notifications key, then run
eas credentials→ [iOS] → [Push Notifications] and upload it. Expo's guide is at https://docs.expo.dev/push-notifications/push-notifications-setup/. - In the [Firebase console], open [Project settings] → [Service accounts], generate a private key, then run
eas credentials→ [Android] → [FCM V1] and upload the JSON. - Check the Android notification icon. The module adds the config plugin
["expo-notifications", { icon: "./assets/images/android-icon-monochrome.png", color: "#ffffff" }], and the status-bar icon has to be monochrome. - Make a dev build and install it on a physical phone:
eas build --profile development, ornpx expo run:ios --device. Since SDK 53 Expo Go doesn't receive remote push, and simulators never do. - There are no
EXPO_PUBLIC_*keys to paste for this module. To send a test by hand, open https://expo.dev/notifications, paste a token, and send.
Going to production?
The APNs key and the FCM v1 service account are per app, not per build profile - upload them once with eas credentials and every profile uses them. Store the tokens your users register on your own server, and send with data: { url: "/some/route" } so a tap lands people where you meant. Delivery errors show up in Expo's push receipts, not in the send response.
- Run
bun run doctor- every row for this module should be green.
The things to set up, in one place (no env keys here):
| What | Where |
|---|---|
| EAS project id | eas init prints it → readynative.config.ts app.easProjectId. https://expo.dev/accounts/[account]/projects |
| APNs key (iOS) | eas credentials → iOS → Push Notifications key. https://docs.expo.dev/push-notifications/push-notifications-setup/ |
| FCM v1 service account | Firebase console → Project settings → Service accounts → upload via eas credentials → Android → FCM V1 |
| Push receipts / dashboard | https://expo.dev/notifications (paste a token to send a test) |
Deps: expo-notifications, expo-device, expo-constants, expo-clipboard, all installed with npx expo install.
Usage
Read the token and the permission status:
import { push } from "@/lib/push";
const { token, status } = push.usePushToken(); // "loading" | "unsupported" | "denied" | "granted"Ask for permission when it makes sense in your flow, not on first launch:
import { push } from "@/lib/push";
const granted = await push.requestPermission(); // true when granted; a token registers when remote push is possiblepush.sendTestNotification({ title, body }) schedules a local notification 3 seconds out (pass a second argument for another delay), asking for permission first; it resolves false when notifications aren't allowed. Settings → Developer tools uses it.
Send one from a server - or from your machine, with the bundled script:
bun run push:test "ExponentPushToken[xxx]" "Title" "Body" --url /settingsThat's a POST to https://exp.host/--/api/v2/push/send with { to, title, body, data: { url } }; data.url is the route the app opens on tap.
Gotchas
- Expo Go can't receive remote push since SDK 53 - no token on Android, and the iOS token only works with Expo's own credentials. The module degrades instead of crashing:
statusbecomesunsupportedordenied. Local notifications still work in Expo Go. push:testonly reports the ticket. Delivery errors likeDeviceNotRegisteredor bad credentials show up in the receipts - follow the link the script prints.- iOS
undeterminedanddeniedboth report asdenied, since the shim has no separate state. The button prompts in the first case and does nothing in the second.
Check it works - steps 2 to 7 use the example screen, so run this in a tree set up with --with-examples:
- Link the EAS project (step 1 of Setup), then
eas credentialsfor the APNs key and FCM v1, theneas build --profile development(ornpx expo run:ios --device) and install it on a physical phone. - Open the app → Examples → Push notifications: the status reads "Permission not granted". Tap Allow notifications → the OS dialog → Allow → toast "Push enabled" and an
ExponentPushToken[...]appears. - Tap Copy token, then on your computer run
bun run push:test "<token>" "Hi" "It works"- it printsSent. Ticket …. - Background the app: the banner arrives within seconds. Foreground it and send again: the banner still shows, thanks to the foreground handler.
- Tap the banner → the app opens on
/examples/pushfromdata.url. Kill the app, send again, tap: the same screen from a cold start. - Tap Schedule local test notification in 5s and background the app: the local banner arrives after 5 seconds and takes you back to the screen. This one works in Expo Go too.
- Deny permission in the OS settings and reopen: the status reads "Permission not granted", nothing crashes, and the button re-prompts (or points to settings on iOS).
Remove it
While modules/ exists (a tree set up with --keep-modules), setup does all of it:
bun run setup --push none --yes --keep-modulesIn a finalized tree setup is a stub, so you undo it by hand. Here is everything this module added:
- Delete the files that are still there (the demo screens are gone already unless you set up with
--with-examples):scripts/push-test.ts,src/app/examples/push.tsx,src/hooks/__tests__/use-push-token.test.tsx,src/hooks/use-push-token.ts,src/lib/__tests__/push.test.tsx,src/screens/examples/push-example-screen.tsx. - Replace, don't delete
src/lib/push.ts: core code imports it, so swap in the no-op version frommodules/push/none/files/of a fresh clone of your tier repo - same exports, nothing behind them. - Uninstall the dependencies:
bun remove expo-notifications. - Drop the config plugin
expo-notificationsfrom.readynative.json→modules.app.expo.plugins(that is whereapp.config.tsreads it from), then rebuild the dev build. - Unwrap the provider: delete
<PushProvider>and its import fromsrc/providers.tsx. - Delete the script
push:testfrompackage.json. - Update the privacy declarations: remove this module's entries from
.readynative.json→modules.app.expo.ios.privacyManifests, then re-runbun run gen:privacyand revise your store privacy answers. - Check it:
bun run typecheckandbun run lintpoint at anything that still imports the removed files;bun run gen:graphrefreshesdocs/ARCHITECTURE.md.
Reference
Everything below is generated from modules/push/expo-notifications/module.json - the same file bun run setup reads, so it is what actually lands in your repo.
Install
bun run setup --push expo-notificationsModule id: push/expo-notifications (the default for this category).
Dependencies
| Package | Version | Kind |
|---|---|---|
expo-clipboard | ~57.0.2 | dependency (expo install) |
expo-constants | ~57.0.19 | dependency (expo install) |
expo-device | ~57.0.2 | dependency (expo install) |
expo-notifications | ~57.0.18 | dependency (expo install) |
Config plugins
Merged into app.config.ts through .readynative.json (modules.app):
expo-notifications(with options)
Privacy
Play Data safety draft: collects Push token (device id); shared with Expo push service, APNs, FCM. Source of truth: vendor disclosure.
Apple privacy manifest data types (composed into ios.privacyManifests by setup):
| Type | Linked to user | Tracking | Purposes |
|---|---|---|---|
DeviceID | yes | no | AppFunctionality |
Providers
Rendered in src/providers.tsx (lower order = outermost):
| Order | Provider | From |
|---|---|---|
| 80 | PushProvider | @/lib/push |
Compatibility
- Requires
storage=kv-store,mmkv,async-storage
Doctor checks
- dev build toolchain (Xcode / Android SDK)
After setup
- Link an EAS project:
bunx eas-cli initprints the project id but cannot write into the dynamic app.config.ts - paste it into readynative.config.tsapp.easProjectId(CI: EAS_PROJECT_ID). app.config.ts derivesextra.eas.projectIdfrom it; getExpoPushTokenAsync needs it, and until then usePushToken() reportsunsupported. - Run
eas credentialsand upload APNs (iOS) and FCM v1 service-account (Android) keys: https://docs.expo.dev/push-notifications/push-notifications-setup/#get-credentials-for-development-builds - Remote push needs a dev build on a physical device (
eas build --profile developmentornpx expo run:ios|android); Expo Go only delivers local notifications. - On a device:
push.requestPermission()then readpush.usePushToken().token(or open /examples/push with--with-examples), and runbun run push:test <token> "Title" "Body"(ornpm run push:test,pnpm run push:test,yarn push:test- it runs on plain node).
Files
7 files copied to the project root
scripts/push-test.tssrc/app/examples/push.tsxsrc/hooks/__tests__/use-push-token.test.tsxsrc/hooks/use-push-token.tssrc/lib/__tests__/push.test.tsxsrc/lib/push.tssrc/screens/examples/push-example-screen.tsx