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 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 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 for background handlers | Doze mode blocks delivery | Use priority |
| 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 | |
| 2 | Android Setup | CRITICAL | |
| 3 | Token Management | HIGH | |
| 4 | Message Handling | HIGH | |
| 5 | Deep Linking | MEDIUM | |
| 6 | Infrastructure | MEDIUM | |
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:
javascript
// 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 for time-sensitive notifications
- Required for Android Doze mode background handlers
Rules
Full documentation with code examples in
:
iOS Setup ()
| File | Impact | Description |
|---|
| CRITICAL | APNs authentication key setup |
| 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 |
| MEDIUM | Method Swizzling management |
| MEDIUM | Notification Service Extension setup |
Android Setup ()
| File | Impact | Description |
|---|
android-notification-channel.md
| CRITICAL | Notification channel creation (Android 8.0+) |
| 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 |
| HIGH | Priority 'high' for Doze mode |
Token Management ()
| File | Impact | Description |
|---|
| HIGH | Token registration with backend |
| HIGH | Token refresh handling |
| MEDIUM | DeviceNotRegistered error handling |
Message Handling ()
| 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 ()
| 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 ()
| File | Impact | Description |
|---|
| MEDIUM | Network firewall configuration |
| MEDIUM | Firebase Installation ID backup exclusion |
| MEDIUM | Push notification rate limiting |
Best Practices
| File | Impact | Description |
|---|
| HIGH | Permission request timing optimization (70-80% acceptance) |
| HIGH | Testing tools, debugging techniques, payload validation |
Searching Rules
bash
# 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 | → |
| Android not receiving push | → android-notification-channel
|
| Push not working in background | → message-background-handler
|
| Push not visible in foreground | or message-foreground-handler
|
| Token missing/expired | → |
| Deep link conflict error | deeplink-navigation-conflict
|
| Notification icon broken (Android) | android-notification-icon
|
| Failed on corporate network | |
| Background handler not called (Android) | → message-data-vs-notification
|
| userNotificationCenter not called (iOS) | |
| 404 error after backup restore | |
| Send failed (429 error) | |
| Low permission acceptance rate | → |
| How to debug/test push | |
| Cannot test locally | → |
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 for background handlers
- Notification icons must be monochrome (white on transparent)
React Native
- Common issue: required for foreground notifications
- Deep linking conflicts with React Navigation
- Terminated state requires native → JS bridge pattern
Expo
- ExpoPushToken format:
- Rate limit: 600 push notifications per second per project
- Development builds required for SDK 53+
- Use for APNs key management
Full Compiled Document
For the complete guide with all rules expanded:
Attribution
Based on push notification troubleshooting guides and Firebase Cloud Messaging official documentation.