Skip to content

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.

Your API → Expo Push API → APNs (iOS) or FCM (Android) → User's phone

Expo 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.

Each phone has a unique Expo Push Token that looks like ExponentPushToken[xxxxxxx]. The flow:

  1. When the user opens your app, you ask for notification permission (expo-notifications).
  2. If granted, you fetch the device’s Expo Push Token.
  3. You send the token to your API.
  4. 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:

  1. Look up the user’s token from Supabase.
  2. Make a POST to https://exp.host/--/api/v2/push/send with the token and a payload.
  3. Expo proxies it to APNs or FCM.

This is one API regardless of platform. No separate iOS and Android code paths.

The starter app has the wiring for asking permission and storing the token already. The pieces:

FileWhat it does
mobile/lib/notifications.tsPermission request, token fetch
mobile/app/_layout.tsxCalls the above on app launch (after sign-in)
api/app/routes/users.pyEndpoint to receive and store the token
Supabase migrationexpo_push_token column on users

The Blueprint card for push notifications walks you through wiring this up if it isn’t already.

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.

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.

For iOS, Expo needs your APNs key to forward notifications to Apple. This is a one-time setup:

  1. In your Apple Developer account, create an APNs Auth Key (Certificates, Identifiers & Profiles → Keys → Add).
  2. Download the .p8 file. Keep this file somewhere safe — you can’t re-download it.
  3. Run eas credentials and 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.

For Android, you need a google-services.json:

  1. 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).
  2. Add an Android app to the project, with your package name (your app’s bundle ID).
  3. Download google-services.json.
  4. Place it at mobile/google-services.json. The starter’s app.json is configured to pick it up.
  5. Run eas credentials to 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.

Easiest way to send a test notification:

Terminal window
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.

  • iOS won’t show notifications when the app is in the foreground by default. Configure Notifications.setNotificationHandler to show them anyway.
  • Android 13+ requires runtime permission for notifications. expo-notifications handles 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.