Push Notifications
Push notifications are mandatory for most consumer apps and the single biggest source of “this seemed like it would be straightforward” for first-time mobile developers. Mosayic uses Expo Push to make it as painless as possible.
The pipeline
Section titled “The pipeline”Your API → Expo Push API → APNs (iOS) or FCM (Android) → User's phoneExpo Push is a thin proxy. You send a JSON payload to it, it forwards to Apple Push Notification service (for iPhones) or Firebase Cloud Messaging (for Android phones), and the user’s phone gets the notification.
You don’t need the Firebase SDK in your app for this to work — Expo handles the FCM side opaquely.
What gets stored where
Section titled “What gets stored where”Each phone has a unique Expo Push Token that looks like ExponentPushToken[xxxxxxx]. The flow:
- When the user opens your app, you ask for notification permission (
expo-notifications). - If granted, you fetch the device’s Expo Push Token.
- You send the token to your API.
- Your API stores the token in the user’s row in Supabase (e.g.
users.expo_push_token).
When you want to send a notification:
- Look up the user’s token from Supabase.
- Make a POST to
https://exp.host/--/api/v2/push/sendwith the token and a payload. - Expo proxies it to APNs or FCM.
This is one API regardless of platform. No separate iOS and Android code paths.
Setting up
Section titled “Setting up”The starter app has the wiring for asking permission and storing the token already. The pieces:
| File | What it does |
|---|---|
mobile/lib/notifications.ts | Permission request, token fetch |
mobile/app/_layout.tsx | Calls the above on app launch (after sign-in) |
api/app/routes/users.py | Endpoint to receive and store the token |
| Supabase migration | expo_push_token column on users |
The Blueprint card for push notifications walks you through wiring this up if it isn’t already.
Sending a notification from your API
Section titled “Sending a notification from your API”import httpx
async def send_push(expo_token: str, title: str, body: str, data: dict | None = None): async with httpx.AsyncClient() as client: await client.post( "https://exp.host/--/api/v2/push/send", json={ "to": expo_token, "sound": "default", "title": title, "body": body, "data": data or {}, }, )That’s it. No SDK, no auth header (Expo accepts requests without keys for now; they may add that later). The response tells you whether the notification was accepted by APNs/FCM.
What you can’t test in the dev client
Section titled “What you can’t test in the dev client”Push notifications don’t work in Expo Go and have specific behaviour in the dev client.
- The dev client can register for notifications and you can send to its token, but APNs and FCM treat it as a “development” build — some features differ from production.
- The production build is what real users get. Behaviour can differ subtly (notification sounds, channel grouping on Android, etc.).
This means you need to test push end-to-end with at least one production build. The Blueprint card recommends doing this once early — right after your first dev build is on your phone — so you don’t discover the “I forgot to upload my APNs key to Expo” mistake on the day of your launch.
APNs setup (iOS)
Section titled “APNs setup (iOS)”For iOS, Expo needs your APNs key to forward notifications to Apple. This is a one-time setup:
- In your Apple Developer account, create an APNs Auth Key (
Certificates, Identifiers & Profiles → Keys → Add). - Download the
.p8file. Keep this file somewhere safe — you can’t re-download it. - Run
eas credentialsand paste the key in when prompted, or upload it via the EAS web UI.
EAS now manages your APNs key on your behalf. You don’t need to think about it again.
FCM setup (Android)
Section titled “FCM setup (Android)”For Android, you need a google-services.json:
- Go to the Firebase Console, create a project (you can use the same one you use for nothing else, or create a new one for FCM specifically).
- Add an Android app to the project, with your package name (your app’s bundle ID).
- Download
google-services.json. - Place it at
mobile/google-services.json. The starter’sapp.jsonis configured to pick it up. - Run
eas credentialsto upload the FCM server key (Firebase → Project settings → Cloud Messaging → Server key).
After this, push works on Android. EAS handles the rest at build time.
Testing push
Section titled “Testing push”Easiest way to send a test notification:
curl -H "Content-Type: application/json" -X POST "https://exp.host/--/api/v2/push/send" \ -d '{ "to": "ExponentPushToken[xxxxxxx]", "title": "Hello", "body": "This is a test" }'If your phone receives the notification, your pipeline works.
There’s also an Expo Push Notifications Tool (a web UI) that does the same thing.
Why Expo Push instead of Firebase directly?
Section titled “Why Expo Push instead of Firebase directly?”You could use Firebase Cloud Messaging directly. Two reasons not to:
- Single API for both platforms. With Expo Push, you write one piece of server code. With direct FCM/APNs, you write two and maintain both.
- Token management. Expo’s tokens stay stable across devices, app updates, and reinstalls in ways FCM tokens don’t.
Expo Push is free for personal use. If you ship at very large scale, the limits become relevant; until then, you don’t need to think about it.
Common gotchas
Section titled “Common gotchas”- iOS won’t show notifications when the app is in the foreground by default. Configure
Notifications.setNotificationHandlerto show them anyway. - Android 13+ requires runtime permission for notifications.
expo-notificationshandles this, but you have to actually ask (requestPermissionsAsync). - Quiet hours. APNs and FCM both rate-limit. Sending more than ~10 notifications per user per day risks getting throttled or marked as spam.
- Production builds vs dev builds use different APNs environments. This is handled by EAS — but if you ever do a manual build, get this wrong and prod notifications won’t deliver.