← Back to skills
extension
Category: Development & EngineeringAPI key requirement unconfirmed

capacitor-push-notifications

Complete guide to implementing push notifications in Capacitor apps using Firebase Cloud Messaging (FCM) and Apple Push Notification Service (APNs). Covers setup, handling, and best practices. Use this skill when users need to add push notifications.

personAuthor: jakexiaohubgithub

Push Notifications in Capacitor

When to Use

TRIGGER when:

  • Adding remote push (FCM / APNs) to a Capacitor app
  • registration / registrationError never 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 (alert presentation 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

  1. Inspect: package.json versions (@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.
  2. Install: npm install @capacitor/push-notifications && npx cap sync.
  3. iOS: capability + APNs key + AppDelegate forwarding. Details: references/ios-setup.md.
  4. Android: google-services.json, channel, icon, permission. Details: references/android-setup.md.
  5. JS: listeners first, then permission, then register() (below).
  6. Backend: send a test message. Payload shapes: references/sending.md.
  7. 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 so pushNotificationActionPerformed from a cold-start tap is captured.
  • Tokens rotate. Send every registration value 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 + list replace the deprecated iOS alert. Capacitor 9 removes alert on iOS; replace it when upgrading. badge is iOS only.
  • Empty array = nothing shown in foreground; handle pushNotificationReceived yourself.

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

  1. iOS: codesign -d --entitlements :- path/to/App.app | grep aps-environment shows development (debug) or production (TestFlight/App Store).
  2. Android: adb shell dumpsys package com.example.app | grep POST_NOTIFICATIONS shows granted=true after accepting the prompt.
  3. App logs a registration token on a real device (iOS Simulator on Apple silicon can receive pushes via xcrun simctl push, but test real APNs on device).
  4. Send a test from Firebase Console (Messaging -> "Send test message") or your backend to that token.
  5. Check all three states: foreground (presentation + pushNotificationReceived), background (system tray), killed (tap -> pushNotificationActionPerformed routes correctly).
  6. 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/