Push Notifications in Capacitor
When to Use
TRIGGER when:
- Adding remote push (FCM / APNs) to a Capacitor app
registration/registrationErrornever fire, or the token works on one platform only- iOS token is a 64-hex APNs token but the backend expects an FCM token
- Notifications arrive in background but not in foreground (or vice versa)
- Android 13+ shows no prompt and nothing is delivered
- Upgrading to Capacitor 8.5 (UIScene) or 9 (
alertpresentation option removed)
Do not use:
- Opening the app from
https:///myapp://links:capacitor-deep-linking - Moving AppDelegate code to SceneDelegate:
capacitor-uiscene-migration - Device log capture:
ios-android-logs - Choosing other native plugins:
capacitor-plugins
Choose the plugin first (ask the user if unclear)
| Need | Plugin |
|------|--------|
| Official plugin; APNs token on iOS, FCM token on Android | @capacitor/push-notifications |
| FCM token on both platforms, topics, Firebase-first backend | @capgo/capacitor-firebase-messaging |
| Capgo-managed delivery (dashboard campaigns, badges, silent live-update checks, no own push server) | @capgo/capacitor-notifications |
| Local notifications only, rich layouts | @capgo/capacitor-rich-notifications or @capacitor/local-notifications |
The rest of this file covers @capacitor/push-notifications. For Capgo plugins follow https://capgo.app/docs/plugins/firebase-messaging/ or https://capgo.app/docs/plugins/notifications/getting-started/.
Workflow
- Inspect:
package.jsonversions (@capacitor/core,@capacitor/push-notifications),capacitor.config.*plugins.PushNotifications,ios/App/App/AppDelegate.swift,ios/App/App/*.entitlements,android/app/google-services.json,android/variables.gradle,AndroidManifest.xml. - Install:
npm install @capacitor/push-notifications && npx cap sync. - iOS: capability + APNs key + AppDelegate forwarding. Details: references/ios-setup.md.
- Android:
google-services.json, channel, icon, permission. Details: references/android-setup.md. - JS: listeners first, then permission, then
register()(below). - Backend: send a test message. Payload shapes: references/sending.md.
- Verify with the checklist below on physical devices.
Only load a reference when its topic is in play.
JS registration (order matters)
import { PushNotifications } from '@capacitor/push-notifications';
export async function initPush() {
// 1. Listeners before register(), or the first token is missed
await PushNotifications.addListener('registration', ({ value }) => sendTokenToServer(value));
await PushNotifications.addListener('registrationError', (err) => console.error('push reg', err.error));
await PushNotifications.addListener('pushNotificationReceived', (n) => {
// foreground only (and Android data-only while app is alive)
});
await PushNotifications.addListener('pushNotificationActionPerformed', ({ notification, actionId }) => {
routeFromNotification(notification.data); // tap handling
});
// 2. Permission (required on iOS and Android 13+ / targetSdk 33+)
let perm = await PushNotifications.checkPermissions();
if (perm.receive === 'prompt' || perm.receive === 'prompt-with-rationale') {
perm = await PushNotifications.requestPermissions();
}
if (perm.receive !== 'granted') return; // explain + link to settings, do not loop prompts
// 3. Register
await PushNotifications.register();
}
Traps:
- Call
initPush()at bootstrap sopushNotificationActionPerformedfrom a cold-start tap is captured. - Tokens rotate. Send every
registrationvalue to the server and upsert by user + device. - On iOS the plugin reports the raw APNs token unless AppDelegate swaps in the FCM token (see ios-setup). Sending an APNs token to FCM fails with
messaging/invalid-registration-token. - Ask permission after a user action that explains the value; iOS shows the system prompt only once.
Foreground presentation
// capacitor.config.ts
plugins: {
PushNotifications: {
presentationOptions: ['badge', 'sound', 'banner', 'list'],
},
},
banner+listreplace the deprecated iOSalert. Capacitor 9 removesalerton iOS; replace it when upgrading.badgeis iOS only.- Empty array = nothing shown in foreground; handle
pushNotificationReceivedyourself.
Capacitor version notes
| Version | Change |
|---------|--------|
| 7 | firebaseMessagingVersion default 24.1.0 |
| 8 | firebaseMessagingVersion default 25.0.1 |
| 8.5 | UIScene lifecycle. Remote-notification callbacks (didRegisterForRemoteNotificationsWithDeviceToken, didFailToRegister...) stay in AppDelegate and keep working. Do not move them to SceneDelegate. Tap routing that used AppDelegate application(_:open:) must move (see capacitor-deep-linking). |
| 9 | iOS alert presentation option removed; iOS 16 min; @main replaces @UIApplicationMain; google-services Gradle plugin 4.5.0 |
Verification
- iOS:
codesign -d --entitlements :- path/to/App.app | grep aps-environmentshowsdevelopment(debug) orproduction(TestFlight/App Store). - Android:
adb shell dumpsys package com.example.app | grep POST_NOTIFICATIONSshowsgranted=trueafter accepting the prompt. - App logs a
registrationtoken on a real device (iOS Simulator on Apple silicon can receive pushes viaxcrun simctl push, but test real APNs on device). - Send a test from Firebase Console (Messaging -> "Send test message") or your backend to that token.
- Check all three states: foreground (presentation +
pushNotificationReceived), background (system tray), killed (tap ->pushNotificationActionPerformedroutes correctly). - iOS simulator smoke test without a server:
cat > /tmp/push.apns <<'EOF'
{ "Simulator Target Bundle": "com.example.app", "aps": { "alert": { "title": "Test", "body": "Hello" } }, "route": "/inbox" }
EOF
xcrun simctl push booted com.example.app /tmp/push.apns
(aps.alert in the APNs payload is the message content and is unaffected by the presentationOptions change.)
Error Handling
| Error / symptom | Cause | Fix |
|-----------------|-------|-----|
| no valid "aps-environment" entitlement string found for application | Push Notifications capability missing or profile not regenerated | Add capability in Xcode, re-sign, rebuild |
| registration never fires on iOS, no error | AppDelegate does not post .capacitorDidRegisterForRemoteNotifications | Add forwarding methods to AppDelegate |
| Default FirebaseApp is not initialized in this process (Android) | google-services.json missing/empty, so google-services plugin was not applied (google-services.json not found, google-services plugin not applied. Push Notifications won't work) | Put file in android/app/, rebuild |
| messaging/registration-token-not-registered | Token stale (app reinstalled / data cleared) | Delete token server-side; rely on next registration |
| messaging/invalid-registration-token / BadDeviceToken | APNs token sent to FCM, or sandbox token sent to production APNs | Use FCM token on iOS (ios-setup) or match APNs environment |
| messaging/third-party-auth-error | APNs key not uploaded to Firebase, wrong Key ID / Team ID | Upload .p8 under Firebase Project Settings -> Cloud Messaging |
| Android 13+: nothing shown, no prompt | requestPermissions() never called | Call it; manifest permission is added by the plugin |
| Android shows white square icon | No monochrome notification icon | Set default_notification_icon meta-data |
| Android notification silent / wrong importance | Channel created with low importance (importance is immutable after creation) | Create a new channel id; delete the old one |
| Data-only message ignored when app killed (Android) | Plugin only delivers while app process lives | Native FirebaseMessagingService (android-setup) |
| iOS silent / background push not delivered to JS | Plugin does not support iOS silent push | Native handling, or @capgo/capacitor-notifications silent update checks |
| Duplicate class errors with Firebase after Capacitor 9 | Old core-ktx / Kotlin stdlib pins | See capacitor-app-upgrade-v8-to-v9 |
Resources
- Plugin API: https://capacitorjs.com/docs/apis/push-notifications
- Firebase guide: https://capacitorjs.com/docs/guides/push-notifications-firebase
- Updating to 9.0: https://capacitorjs.com/docs/updating/9-0
- Capgo Firebase Messaging: https://capgo.app/docs/plugins/firebase-messaging/
- Capgo Notifications: https://capgo.app/docs/plugins/notifications/
Scan to join WeChat group