Push notifications
What it is
Push notifications reach phones through Firebase Cloud Messaging (FCM). On Android, FCM delivers them itself. On iOS, FCM hands them to Apple's APNs (Apple Push Notification service), so Firebase needs a key from the Apple Developer account. Each installed app gets a device token; the backend sends to that token.
Why we do it
Push setup touches three consoles (Firebase, Apple, the app's build settings) and fails silently when one is missing: notifications work on Android and never on iOS, or only in dev.
Before you start
- The Apple Developer account (in the client's name) is active, from step 2.
- Bundle ids and package names per flavor are final. See Identifiers and signing.
- A Google account using the team mailbox, for Firebase.
How it fits together
Steps
1. One Firebase project per environment
In the Firebase console create acme-dev, acme-staging, acme-prod. In each, add:
- an Android app with that flavor's package name, and download
google-services.json - an iOS app with that flavor's bundle id, and download
GoogleService-Info.plist
For Flutter, flutterfire configure per flavor writes these for you. Put each file in the
flavor's folder so a dev build can never talk to the prod project.
2. Create the APNs key in Apple Developer
- Certificates, Identifiers & Profiles, Keys, +.
- Name it
acme APNs, tick Apple Push Notifications service (APNs), continue, register. - Download the
.p8file. Apple lets you download it only once. Store it in the team vault with its Key ID and the account's Team ID.
One key works for all apps in the Apple account and for both sandbox and production.
3. Give the key to Firebase
In each Firebase project: Project settings, Cloud Messaging, Apple app configuration, APNs Authentication Key, Upload. Enter the Key ID and Team ID.
4. Turn on the capability in the app
- iOS (Xcode, Runner target, Signing & Capabilities): add Push Notifications and Background Modes with Remote notifications. Then regenerate the provisioning profile.
- Android 13 and later: ask for the
POST_NOTIFICATIONSpermission at a sensible moment, not on first launch. - Ask for permission when the user does something that benefits from it, and handle "no".
5. Let the backend send
- In each Firebase project: Project settings, Service accounts, Generate new private key. This JSON is a secret.
- Store it in SSM:
/acme/<env>/api/FIREBASE_SERVICE_ACCOUNT. - Send with the Firebase Admin SDK, which uses the FCM HTTP v1 API:
import { initializeApp, cert } from 'firebase-admin/app';
import { getMessaging } from 'firebase-admin/messaging';
initializeApp({ credential: cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT!)) });
try {
await getMessaging().send({ token, notification: { title, body }, data: { route: '/orders/123' } });
} catch (e: any) {
if (e.code === 'messaging/registration-token-not-registered') await tokensRepo.delete(token);
else throw e;
}
Delete tokens Firebase reports as not registered; they belong to uninstalled apps.
Check it worked
- Install the dev build on a real iPhone and a real Android phone (iOS simulators have limits with push).
- Copy each device token from the app's debug screen or logs.
- Firebase console, Messaging, New campaign, Send test message to the token.
- Then send from the dev API to the same tokens.
If it goes wrong
| You see | It means |
|---|---|
| Android works, iOS never does | APNs key not uploaded to that Firebase project, or the capability is missing |
| Works in dev, not in prod | Prod Firebase project lacks the APNs key, or the prod app uses dev config files |
registration-token-not-registered | App uninstalled or token rotated. Delete the token |
| iOS shows nothing, no error | The user refused permission, or the app is in the foreground and does not display it |
Rules
Promotional push needs an explicit opt-in (Apple Guideline 4.5.4). Transactional push (your order shipped) does not, but must be about the user's own activity.
The service account JSON is a server secret. It never goes into the app.
Done when
- Three Firebase projects, each with Android and iOS apps and their config files per flavor.
- APNs key created, stored in the vault, uploaded to all three projects.
- Capabilities on; permission asked at a sensible moment.
- Service account in SSM; backend deletes dead tokens.
- A test push arrived on a real iPhone and a real Android phone from dev.