push-notification-best-practices
Push Notification Best Practices
Overview
Comprehensive guide for implementing and troubleshooting push notifications in mobile applications. Covers iOS (APNS), Android (FCM), React Native, Expo, and Flutter platforms with platform-specific configurations, token management, message handling, and deep linking patterns.
Platform Support Matrix
| Platform | Offline Push | Notification Bar | Click Navigation | In-App Toast | Background Handler |
|---|---|---|---|---|---|
| iOS | APNS | System | Deep Link | Custom | data-only payload |
| Android | FCM | System | Intent | Custom | data-only payload |
| React Native | APNS/FCM | System | React Navigation | Custom | setBackgroundMessageHandler |
| Expo | Expo Push | System | Linking | Custom | TaskManager |
| Flutter | APNS/FCM | System | Navigator | Custom | onBackgroundMessage |
Decision Trees
Permission Request Timing
When to request notification permission?
User installing app
|
v
[First launch?] ──Yes──> [Show value proposition first]
| |
No v
| [User action triggers need?]
v |
[Already granted?] Yes
| |
Yes v
| [Request permission] ──> 70-80% acceptance rate
v
[Notifications ready]
| Timing | Acceptance Rate | Use Case |
|---|---|---|
| Immediate (app launch) | 15-20% | Low engagement apps |
| After onboarding | 40-50% | Standard apps |
| User-initiated action | 70-80% | High engagement apps |
Recommendation: Request after explaining value or when user enables a related feature.
Silent vs Visible Notification
Which payload type should I use?
[What's the purpose?]
|
+──> Time-sensitive user alert ──> Visible (notification payload)
|
+──> Background data sync ──> Silent (data-only payload)
|
+──> Custom UI required ──> Silent (data-only payload)
|
+──> Need background processing ──> Silent (data-only payload)
| Scenario | Payload Type | Reason |
|---|---|---|
| New message alert | Visible | User needs immediate attention |
| Order status update | Visible | Time-sensitive information |
| Background sync | Silent | No user interruption needed |
| Custom notification UI | Silent | Full control over display |
| Update badge count | Silent | Background processing needed |
Extension Strategy (iOS)
When to use Notification Service Extension?
- Need to modify notification content before display
- Need to download and attach media (images, videos)
- Need to decrypt end-to-end encrypted payloads
- Need to add action buttons dynamically
When to use Notification Content Extension?
- Need custom notification UI beyond system template
- Need interactive elements in expanded view
Anti-patterns (NEVER Do)
Permission Handling
| NEVER | Why | Instead |
|---|---|---|
| Request permission on first launch without context | 15-20% acceptance rate | Explain value first, then request |
| Re-ask after user denies | System ignores repeated requests | Show settings redirect |
| Ignore provisional authorization | Misses iOS 12+ quiet delivery | Use .provisional option |
Token Management
| NEVER | Why | Instead |
|---|---|---|
| Cache tokens long-term | Tokens can change on reinstall/restore | Always use fresh token from callback |
| Assume token format | Format varies by platform/SDK | Treat as opaque string |
| Send token without user association | Can't target notifications | Associate with userId on backend |
| Store tokens without device ID | Duplicate tokens per user | Use deviceId as unique key |
Message Handling
| NEVER | Why | Instead |
|---|---|---|
Use notification payload for background processing |
onMessageReceived not called in background | Use data-only payload |
| Rely on silent notifications for time-critical delivery | Delivery not guaranteed | Use visible notifications |
| Execute heavy operations in background handler | System kills app after ~30 seconds | Queue work, process quickly |
| Forget to handle both payload types | Missing notifications | Handle notification + data payloads |
iOS Specific
| NEVER | Why | Instead |
|---|---|---|
| Register for notifications before delegate setup | Delegate methods not called | Set delegate before registerForRemoteNotifications() |
Skip serviceExtensionTimeWillExpire() implementation |
Content modification fails | Always implement fallback |
| Use .p12 certificates | Expires yearly, deprecated | Use .p8 authentication key |
Android Specific
| NEVER | Why | Instead |
|---|---|---|
| Skip NotificationChannel creation | Notifications don't appear on Android 8.0+ | Create channel at app start |
Use priority normal for background handlers |
Doze mode blocks delivery | Use priority high |
| Use colored notification icons | Android ignores colors, shows white square | Use white-on-transparent icons |
Skill Format
Each rule file follows a hybrid format for fast lookup and deep understanding:
- Quick Pattern: Incorrect/Correct code snippets for immediate pattern matching
- When to Apply: Situations where this rule is relevant
- Deep Dive: Full context with step-by-step guides and platform-specific details
- Common Pitfalls: Common mistakes and how to avoid them
- Related Rules: Cross-references to related patterns
Impact ratings: CRITICAL (fix immediately), HIGH (significant improvement), MEDIUM (worthwhile optimization)
When to Apply
Reference these guidelines when:
- Setting up push notifications in a mobile app
- Debugging push notification delivery issues
- Implementing background notification handlers
- Integrating deep linking with push notifications
- Managing push tokens and device registration
- Troubleshooting platform-specific push issues
- Reviewing push notification code for reliability
Priority-Ordered Guidelines
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | iOS Setup | CRITICAL | ios- |
| 2 | Android Setup | CRITICAL | android- |
| 3 | Token Management | HIGH | token- |
| 4 | Message Handling | HIGH | message- |
| 5 | Deep Linking | MEDIUM | deeplink- |
| 6 | Infrastructure | MEDIUM | infra- |
Quick Reference
Critical: iOS Setup
Setup checklist:
- Generate APNs authentication key (.p8) from Apple Developer
- Upload to Firebase Console with Key ID and Team ID
- Enable Push Notifications capability in Xcode
- Set UNUserNotificationCenter delegate in didFinishLaunchingWithOptions
- Request notification authorization before registering
- Implement foreground presentation options
Critical: Android Setup
Setup checklist:
- Add google-services.json to app/ directory
- Apply google-services Gradle plugin
- Create NotificationChannel (Android 8.0+)
- Request POST_NOTIFICATIONS permission (Android 13+)
- Implement FirebaseMessagingService
- Set notification icon (transparent background + white)
- Use priority 'high' for Doze mode compatibility
High: Token Management
Token lifecycle:
// 1. Register token with server
const token = await getToken();
await registerToken(token, userId);
// 2. Handle token refresh
onTokenRefresh((newToken) => {
updateTokenOnServer(newToken, userId);
});
// 3. Handle invalidation
if (error.code === 'DeviceNotRegistered') {
deleteTokenFromDatabase(token);
}
High: Message Handling
Payload types:
- data-only: Background handler runs on both platforms
- notification + data: iOS/Android behavior differs
- iOS: Always shows notification
- Android: Background = system tray, Foreground = onMessageReceived
Priority settings:
- Use
priority: 'high'for time-sensitive notifications - Required for Android Doze mode background handlers
Rules
Full documentation with code examples in rules/:
iOS Setup (ios-*)
| File | Impact | Description |
|---|---|---|
ios-apns-auth-key.md |
CRITICAL | APNs authentication key setup |
ios-delegate-setup.md |
CRITICAL | UNUserNotificationCenter delegate configuration |
ios-permission-request.md |
CRITICAL | Notification permission request flow |
ios-token-registration.md |
HIGH | APNS token registration and handling |
ios-foreground-display.md |
HIGH | Display notifications in foreground |
ios-method-swizzling.md |
MEDIUM | Method Swizzling management |
ios-extension-build.md |
MEDIUM | Notification Service Extension setup |
Android Setup (android-*)
| File | Impact | Description |
|---|---|---|
android-notification-channel.md |
CRITICAL | Notification channel creation (Android 8.0+) |
android-permission.md |
CRITICAL | POST_NOTIFICATIONS runtime permission (Android 13+) |
android-google-services.md |
CRITICAL | Firebase project configuration |
android-messaging-service.md |
HIGH | FirebaseMessagingService implementation |
android-notification-icon.md |
MEDIUM | Notification icon asset requirements |
android-priority-high.md |
HIGH | Priority 'high' for Doze mode |
Token Management (token-*)
| File | Impact | Description |
|---|---|---|
token-registration.md |
HIGH | Token registration with backend |
token-refresh.md |
HIGH | Token refresh handling |
token-invalidation.md |
MEDIUM | DeviceNotRegistered error handling |
Message Handling (message-*)
| File | Impact | Description |
|---|---|---|
message-data-vs-notification.md |
CRITICAL | data vs notification payload differences |
message-background-handler.md |
HIGH | Background message processing |
message-foreground-handler.md |
HIGH | Foreground notification display |
Deep Linking (deeplink-*)
| File | Impact | Description |
|---|---|---|
deeplink-navigation-conflict.md |
MEDIUM | React Navigation conflict resolution |
deeplink-terminated-state.md |
MEDIUM | Deep link handling when app is terminated |
Infrastructure (infra-*)
| File | Impact | Description |
|---|---|---|
infra-firewall-ports.md |
MEDIUM | Network firewall configuration |
infra-backup-fid.md |
MEDIUM | Firebase Installation ID backup exclusion |
infra-rate-limiting.md |
MEDIUM | Push notification rate limiting |
Best Practices
| File | Impact | Description |
|---|---|---|
permission-timing.md |
HIGH | Permission request timing optimization (70-80% acceptance) |
testing-debugging.md |
HIGH | Testing tools, debugging techniques, payload validation |
Searching Rules
# Find rules by keyword
grep -l "apns" rules/
grep -l "fcm" rules/
grep -l "token" rules/
grep -l "background" rules/
grep -l "permission" rules/
grep -l "deeplink" rules/
Problem → Rule Mapping
| Problem | Start With |
|---|---|
| iOS not receiving push | ios-apns-auth-key → ios-delegate-setup |
| Android not receiving push | android-google-services → android-notification-channel |
| Push not working in background | android-priority-high → message-background-handler |
| Push not visible in foreground | ios-foreground-display or message-foreground-handler |
| Token missing/expired | token-registration → token-refresh |
| Deep link conflict error | deeplink-navigation-conflict |
| Notification icon broken (Android) | android-notification-icon |
| Failed on corporate network | infra-firewall-ports |
| Background handler not called (Android) | android-priority-high → message-data-vs-notification |
| userNotificationCenter not called (iOS) | ios-delegate-setup |
| 404 error after backup restore | infra-backup-fid |
| Send failed (429 error) | infra-rate-limiting |
| Low permission acceptance rate | permission-timing → ios-permission-request |
| How to debug/test push | testing-debugging |
| Cannot test locally | testing-debugging → ios-apns-auth-key |
Platform-Specific Notes
iOS (APNS)
- Requires Apple Developer account and APNs authentication key (.p8)
- Different behavior for Development vs Production builds
- Delegate must be set before other SDK initialization
- Method Swizzling can interfere with delegate calls
Android (FCM)
- Requires google-services.json from Firebase Console
- NotificationChannel required for Android 8.0+
- Runtime permission required for Android 13+
- Doze mode requires
priority: 'high'for background handlers - Notification icons must be monochrome (white on transparent)
React Native
- Common issue:
gcm.message_idrequired for foreground notifications - Deep linking conflicts with React Navigation
- Terminated state requires native → JS bridge pattern
Expo
- ExpoPushToken format:
ExponentPushToken[...] - Rate limit: 600 push notifications per second per project
- Development builds required for SDK 53+
- Use
eas credentialsfor APNs key management
Full Compiled Document
For the complete guide with all rules expanded: AGENTS.md
Attribution
Based on push notification troubleshooting guides and Firebase Cloud Messaging official documentation.