Push uses two providers: FCM (Firebase Cloud Messaging) for Android and the web, and APNs (Apple Push Notification service) for iOS. AppEngine wraps both behind the same Broadcast surface — register tokens, target devices, send.
Connect FCM and APNs
Both go through Upstream as separate vendors:
- FCMProvider — needs a Firebase service-account JSON
- APNsProvider — needs the team ID, key ID, bundle ID, and the .p8 auth key
Connect via:
/upstream/save-integrationJWT{ "type": "FCMProvider", "config": { "serviceAccount": { /* paste JSON */ } } }
{
"type": "APNsProvider",
"config": {
"teamId": "ABCD12EFGH",
"keyId": "XYZ987",
"bundleId": "com.acme.app",
"authKey": "<contents of AuthKey_XYZ987.p8>",
"environment": "production"
}
}
Test each integration with POST /upstream/test/{integration}/sendTest.
Register a device token
The mobile app or web app obtains a token from the platform SDK (FCM getToken(), web Notification API, or APNs application:didRegisterForRemoteNotificationsWithDeviceToken:) and POSTs it to AppEngine:
/repository/create/push_tokenJWT{
"data": {
"platform": "ios",
"token": "<APNs token>",
"userId": "user-abc",
"deviceId": "iPhone-stable-uuid",
"appVersion": "1.4.2",
"topics": ["news", "promotions"]
}
}
Fields:
| Field | Notes |
|---|---|
platform | ios | android | web |
token | Provider-specific token. APNs is hex; FCM is opaque base64. |
userId | Optional — link the token to a logged-in user / customer |
deviceId | Stable identifier so re-registrations replace the old token |
topics | List of topics this device should receive |
appVersion, osVersion, model | Optional metadata |
Re-register on every app launch (tokens can rotate). The endpoint deduplicates by deviceId so refreshes don't pile up.
Sending to a single device
Use the broadcast pipeline with channel: 'push' and a recipient list of token ids, or send directly through the Upstream call interface for one-off sends:
/upstream/call/FCMProvider/sendNotificationJWT{
"data": {
"token": "<fcm token>",
"title": "New message",
"body": "Alice replied to your comment",
"data": { "threadId": "abc-123", "deepLink": "/threads/abc-123" }
}
}
Topic broadcasts
Topics are server-side mailing lists. A device subscribed to topic:news receives anything sent to that topic without you tracking individual tokens.
{
"data": {
"broadcastName": "Big news",
"channel": "push",
"type": "push",
"topic": "news",
"title": "Acme launches v2",
"body": "Tap to read what's new",
"data": { "deepLink": "/announcements/v2" }
}
}
Topic membership is managed through the push_token record's topics array. Subscribe by updating the record; AppEngine syncs membership to FCM topic subscriptions on the next send.
APNs doesn't have topics natively — AppEngine fans the topic out into per-token sends behind the scenes.
Per-user broadcasts
For "send to every device of every user matching segment X", use the segment recipient method:
{
"data": {
"broadcastName": "Re-engage dormant users",
"channel": "push",
"type": "push",
"title": "We miss you",
"body": "Come back and see what's new",
"recipientMethod": "segment",
"selectedSegments": ["dormant-30d"],
"data": { "deepLink": "/" }
}
}
The pipeline expands the segment to contacts, finds every push_token linked by userId, and sends one push per token. Rate limits are FCM's per-project (~600k req/min) and APNs' per-connection (concurrent streams) — AppEngine paces the sends to stay under both.
Payload size
Both providers cap notification payloads:
- FCM — 4 KB total
- APNs — 4 KB for normal alerts, 5 KB for VoIP, 256 bytes for Apple Watch complications
Keep title and body short; put structured data in data and let the app render the rich UI from a deep link or local fetch.
Silent (data-only) pushes
Sometimes you want the app to wake up and refresh without showing an alert.
{ "channel": "push", "silent": true, "data": { "syncRequest": "messages" } }
The pipeline flags content_available for APNs and a data-only payload for FCM. Background execution time is short (30s typical) so the app should kick off any work and acknowledge quickly.
Failure handling
When a token is unregistered (uninstall, signed out) the provider returns an error. The pipeline marks the push_token as inactive: true and stops trying. Re-activation happens automatically when the app re-registers with the same deviceId.
Bounce reasons are recorded in the broadcast_delivery row so per-campaign analytics show how many devices were stale.
For VoIP and rich-media pushes (images, action buttons) the same pipeline is used — fields like mutableContent, category, and attachments are passed through to APNs. FCM equivalent fields go in the android.notification and webpush.notification blocks.