Push Notification Architecture
Introduction to Push Notifications
Push notifications are a critical component of modern mobile applications, enabling real-time communication with users even when the app is not active. They provide a way to re-engage users, deliver timely information, and maintain app relevance.
Platform-Specific Implementation
iOS Push Notifications (APNs)
Basic APNs Integration
swift
import UserNotifications
import UIKit
class PushNotificationManager: NSObject {
static let shared = PushNotificationManager()
private override init() {
super.init()
}
func registerForPushNotifications() {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .sound, .badge, .provisional]
) { [weak self] granted, error in
print("Push notification permission granted: \(granted)")
guard granted else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
func handleDeviceToken(_ deviceToken: Data) {
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
let token = tokenParts.joined()
print("Device Token: \(token)")
// Send token to your server
sendTokenToServer(token)
}
private func sendTokenToServer(_ token: String) {
let endpoint = URL(string: "https://api.yourapp.com/devices/register")!
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let payload = [
"device_token": token,
"platform": "ios",
"app_version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: payload)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Failed to register device token: \(error)")
} else {
print("Device token registered successfully")
}
}.resume()
} catch {
print("Failed to serialize device token payload: \(error)")
}
}
}
// AppDelegate integration
extension AppDelegate {
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
PushNotificationManager.shared.handleDeviceToken(deviceToken)
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("Failed to register for remote notifications: \(error)")
}
}
extension PushNotificationManager: UNUserNotificationCenterDelegate {
// Handle notification when app is in foreground
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
let userInfo = notification.request.content.userInfo
handleNotificationData(userInfo)
// Show notification even when app is in foreground
completionHandler([.alert, .sound, .badge])
}
// Handle notification tap
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
let actionIdentifier = response.actionIdentifier
switch actionIdentifier {
case UNNotificationDefaultActionIdentifier:
// User tapped the notification
handleNotificationTap(userInfo)
case "ACCEPT_ACTION":
handleAcceptAction(userInfo)
case "DECLINE_ACTION":
handleDeclineAction(userInfo)
default:
break
}
completionHandler()
}
private func handleNotificationData(_ userInfo: [AnyHashable: Any]) {
// Parse notification data and update app state
if let notificationType = userInfo["type"] as? String {
NotificationRouter.shared.route(type: notificationType, data: userInfo)
}
}
private func handleNotificationTap(_ userInfo: [AnyHashable: Any]) {
// Navigate to specific screen based on notification content
if let deepLink = userInfo["deep_link"] as? String {
DeepLinkHandler.shared.handle(deepLink)
}
}
}
Rich Notifications with Media
swift
class RichNotificationManager {
func scheduleRichNotification(
title: String,
body: String,
imageURL: String,
identifier: String
) {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
// Add image attachment
if let url = URL(string: imageURL) {
downloadImageAndAttach(url: url, to: content) { [weak self] in
let request = UNNotificationRequest(
identifier: identifier,
content: content,
trigger: nil
)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Failed to schedule rich notification: \(error)")
}
}
}
}
}
private func downloadImageAndAttach(
url: URL,
to content: UNMutableNotificationContent,
completion: @escaping () -> Void
) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data,
let image = UIImage(data: data),
let jpegData = image.jpegData(compressionQuality: 0.8) else {
completion()
return
}
let tempDirectory = NSTemporaryDirectory()
let fileName = url.lastPathComponent
let fileURL = URL(fileURLWithPath: tempDirectory).appendingPathComponent(fileName)
do {
try jpegData.write(to: fileURL)
let attachment = try UNNotificationAttachment(
identifier: "image",
url: fileURL,
options: nil
)
content.attachments = [attachment]
} catch {
print("Failed to create notification attachment: \(error)")
}
completion()
}.resume()
}
}
Android Push Notifications (FCM)
Firebase Cloud Messaging Setup
kotlin
class PushNotificationManager {
companion object {
private const val TAG = "PushNotificationManager"
}
fun initialize(context: Context) {
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.w(TAG, "Fetching FCM registration token failed", task.exception)
return@addOnCompleteListener
}
// Get new FCM registration token
val token = task.result
Log.d(TAG, "FCM Registration Token: $token")
// Send token to your app server
sendTokenToServer(context, token)
}
// Subscribe to topics
subscribeToTopics()
}
private fun sendTokenToServer(context: Context, token: String) {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.yourapp.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiService = retrofit.create(ApiService::class.java)
val deviceInfo = DeviceRegistration(
token = token,
platform = "android",
appVersion = getAppVersion(context),
deviceModel = Build.MODEL,
osVersion = Build.VERSION.RELEASE
)
apiService.registerDevice(deviceInfo).enqueue(object : Callback<ApiResponse> {
override fun onResponse(call: Call<ApiResponse>, response: Response<ApiResponse>) {
if (response.isSuccessful) {
Log.d(TAG, "Device token registered successfully")
} else {
Log.e(TAG, "Failed to register device token: ${response.code()}")
}
}
override fun onFailure(call: Call<ApiResponse>, t: Throwable) {
Log.e(TAG, "Network error registering device token", t)
}
})
}
private fun subscribeToTopics() {
// Subscribe to general notifications
FirebaseMessaging.getInstance().subscribeToTopic("general")
.addOnCompleteListener { task ->
val msg = if (task.isSuccessful) "Subscribed to general topic" else "Failed to subscribe"
Log.d(TAG, msg)
}
// Subscribe to user-specific topics based on preferences
val userPreferences = getUserNotificationPreferences()
userPreferences.forEach { topic ->
FirebaseMessaging.getInstance().subscribeToTopic(topic)
}
}
fun unsubscribeFromTopic(topic: String) {
FirebaseMessaging.getInstance().unsubscribeFromTopic(topic)
.addOnCompleteListener { task ->
val msg = if (task.isSuccessful) "Unsubscribed from $topic" else "Failed to unsubscribe from $topic"
Log.d(TAG, msg)
}
}
}
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
Log.d(TAG, "Refreshed token: $token")
// Send the new token to your app server
PushNotificationManager().sendTokenToServer(this, token)
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
Log.d(TAG, "From: ${remoteMessage.from}")
// Handle data payload
if (remoteMessage.data.isNotEmpty()) {
Log.d(TAG, "Message data payload: ${remoteMessage.data}")
handleDataMessage(remoteMessage.data)
}
// Handle notification payload
remoteMessage.notification?.let {
Log.d(TAG, "Message Notification Body: ${it.body}")
showNotification(it.title, it.body, remoteMessage.data)
}
}
private fun handleDataMessage(data: Map<String, String>) {
val notificationType = data["type"]
val notificationData = data["data"]
when (notificationType) {
"message" -> handleNewMessage(notificationData)
"update" -> handleAppUpdate(notificationData)
"promotion" -> handlePromotion(notificationData)
else -> showGenericNotification(data)
}
}
private fun showNotification(title: String?, body: String?, data: Map<String, String>) {
val intent = Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
// Add extra data for deep linking
data.forEach { (key, value) ->
putExtra(key, value)
}
}
val pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
val channelId = "default_channel"
val notificationBuilder = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title ?: getString(R.string.app_name))
.setContentText(body ?: "New notification")
.setAutoCancel(true)
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
// Add action buttons if needed
addNotificationActions(notificationBuilder, data)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Create notification channel for Android O and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"Default Channel",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Default notification channel"
enableLights(true)
lightColor = Color.BLUE
enableVibration(true)
}
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(System.currentTimeMillis().toInt(), notificationBuilder.build())
}
private fun addNotificationActions(
builder: NotificationCompat.Builder,
data: Map<String, String>
) {
val notificationType = data["type"]
when (notificationType) {
"message" -> {
// Add reply action
val replyIntent = Intent(this, QuickReplyService::class.java)
val replyPendingIntent = PendingIntent.getService(
this, 0, replyIntent, PendingIntent.FLAG_IMMUTABLE
)
builder.addAction(
R.drawable.ic_reply,
"Reply",
replyPendingIntent
)
}
"friend_request" -> {
// Add accept/decline actions
val acceptIntent = Intent(this, FriendRequestService::class.java).apply {
putExtra("action", "accept")
putExtra("request_id", data["request_id"])
}
val acceptPendingIntent = PendingIntent.getService(
this, 1, acceptIntent, PendingIntent.FLAG_IMMUTABLE
)
val declineIntent = Intent(this, FriendRequestService::class.java).apply {
putExtra("action", "decline")
putExtra("request_id", data["request_id"])
}
val declinePendingIntent = PendingIntent.getService(
this, 2, declineIntent, PendingIntent.FLAG_IMMUTABLE
)
builder.addAction(R.drawable.ic_check, "Accept", acceptPendingIntent)
builder.addAction(R.drawable.ic_close, "Decline", declinePendingIntent)
}
}
}
companion object {
private const val TAG = "MyFirebaseMsgService"
}
}
React Native Push Notifications
Using @react-native-firebase/messaging
typescript
import messaging from '@react-native-firebase/messaging';
import notifee, { AndroidImportance } from '@notifee/react-native';
import { Platform } from 'react-native';
class PushNotificationManager {
async initialize(): Promise<void> {
// Request permission
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) {
console.log('Push notification permission granted');
await this.setupNotificationHandlers();
await this.getToken();
}
}
async getToken(): Promise<string | null> {
try {
const token = await messaging().getToken();
console.log('FCM Token:', token);
// Send token to your server
await this.sendTokenToServer(token);
return token;
} catch (error) {
console.error('Failed to get FCM token:', error);
return null;
}
}
private async sendTokenToServer(token: string): Promise<void> {
try {
const response = await fetch('https://api.yourapp.com/devices/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token,
platform: Platform.OS,
appVersion: '1.0.0', // Get from app config
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
console.log('Token registered successfully');
} catch (error) {
console.error('Failed to register token:', error);
}
}
private async setupNotificationHandlers(): Promise<void> {
// Handle background messages
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
console.log('Message handled in the background!', remoteMessage);
await this.displayNotification(remoteMessage);
});
// Handle foreground messages
messaging().onMessage(async (remoteMessage) => {
console.log('A new FCM message arrived!', remoteMessage);
await this.displayNotification(remoteMessage);
});
// Handle notification open app
messaging().onNotificationOpenedApp((remoteMessage) => {
console.log('Notification caused app to open from background state:', remoteMessage);
this.handleNotificationNavigation(remoteMessage);
});
// Check whether an initial notification is available
messaging()
.getInitialNotification()
.then((remoteMessage) => {
if (remoteMessage) {
console.log('Notification caused app to open from quit state:', remoteMessage);
this.handleNotificationNavigation(remoteMessage);
}
});
// Handle token refresh
messaging().onTokenRefresh((token) => {
console.log('FCM token refreshed:', token);
this.sendTokenToServer(token);
});
}
private async displayNotification(remoteMessage: any): Promise<void> {
const { notification, data } = remoteMessage;
// Create a channel (required for Android)
const channelId = await notifee.createChannel({
id: 'default',
name: 'Default Channel',
importance: AndroidImportance.HIGH,
});
// Display notification
await notifee.displayNotification({
title: notification?.title || 'New Notification',
body: notification?.body || 'You have a new message',
data: data,
android: {
channelId,
smallIcon: 'ic_launcher',
largeIcon: data?.image_url,
pressAction: {
id: 'default',
},
actions: this.getNotificationActions(data),
},
ios: {
attachments: data?.image_url ? [{ url: data.image_url }] : [],
},
});
}
private getNotificationActions(data: any): any[] {
const actions = [];
switch (data?.type) {
case 'message':
actions.push({
title: 'Reply',
pressAction: { id: 'reply' },
});
break;
case 'friend_request':
actions.push(
{
title: 'Accept',
pressAction: { id: 'accept' },
},
{
title: 'Decline',
pressAction: { id: 'decline' },
}
);
break;
}
return actions;
}
private handleNotificationNavigation(remoteMessage: any): void {
const { data } = remoteMessage;
// Navigate based on notification data
if (data?.screen) {
// Use your navigation library to navigate
// NavigationService.navigate(data.screen, data.params);
}
}
async subscribeToTopic(topic: string): Promise<void> {
try {
await messaging().subscribeToTopic(topic);
console.log(`Subscribed to topic: ${topic}`);
} catch (error) {
console.error(`Failed to subscribe to topic ${topic}:`, error);
}
}
async unsubscribeFromTopic(topic: string): Promise<void> {
try {
await messaging().unsubscribeFromTopic(topic);
console.log(`Unsubscribed from topic: ${topic}`);
} catch (error) {
console.error(`Failed to unsubscribe from topic ${topic}:`, error);
}
}
}
// Usage
const pushNotificationManager = new PushNotificationManager();
pushNotificationManager.initialize();
Flutter Push Notifications
Using firebase_messaging package
dart
class PushNotificationManager {
static final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
static Future<void> initialize() async {
// Request permission
NotificationSettings settings = await _firebaseMessaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
print('User granted permission');
await _setupMessageHandlers();
await _getToken();
} else {
print('User declined or has not accepted permission');
}
}
static Future<void> _getToken() async {
try {
String? token = await _firebaseMessaging.getToken();
print('FCM Token: $token');
if (token != null) {
await _sendTokenToServer(token);
}
} catch (e) {
print('Failed to get FCM token: $e');
}
}
static Future<void> _sendTokenToServer(String token) async {
try {
final response = await http.post(
Uri.parse('https://api.yourapp.com/devices/register'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'token': token,
'platform': Platform.operatingSystem,
'app_version': '1.0.0',
}),
);
if (response.statusCode == 200) {
print('Token registered successfully');
} else {
print('Failed to register token: ${response.statusCode}');
}
} catch (e) {
print('Error registering token: $e');
}
}
static Future<void> _setupMessageHandlers() async {
// Handle background messages
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Handle foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('Got a message whilst in the foreground!');
print('Message data: ${message.data}');
if (message.notification != null) {
print('Message also contained a notification: ${message.notification}');
_showLocalNotification(message);
}
});
// Handle notification taps when app is in background
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print('A new onMessageOpenedApp event was published!');
_handleNotificationTap(message);
});
// Handle notification taps when app is terminated
RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
_handleNotificationTap(initialMessage);
}
// Handle token refresh
FirebaseMessaging.instance.onTokenRefresh.listen((String token) {
print('FCM token refreshed: $token');
_sendTokenToServer(token);
});
}
static Future<void> _showLocalNotification(RemoteMessage message) async {
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'default_channel',
'Default Channel',
channelDescription: 'Default notification channel',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
);
const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);
await FlutterLocalNotificationsPlugin().show(
message.hashCode,
message.notification?.title ?? 'New Notification',
message.notification?.body ?? 'You have a new message',
platformChannelSpecifics,
payload: jsonEncode(message.data),
);
}
static void _handleNotificationTap(RemoteMessage message) {
final data = message.data;
if (data.containsKey('screen')) {
// Navigate to specific screen
NavigationService.navigateTo(data['screen'], arguments: data);
}
}
static Future<void> subscribeToTopic(String topic) async {
try {
await _firebaseMessaging.subscribeToTopic(topic);
print('Subscribed to topic: $topic');
} catch (e) {
print('Failed to subscribe to topic $topic: $e');
}
}
static Future<void> unsubscribeFromTopic(String topic) async {
try {
await _firebaseMessaging.unsubscribeFromTopic(topic);
print('Unsubscribed from topic: $topic');
} catch (e) {
print('Failed to unsubscribe from topic $topic: $e');
}
}
}
// Background message handler
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print("Handling a background message: ${message.messageId}");
// Process the message data
await _processBackgroundMessage(message);
}
Future<void> _processBackgroundMessage(RemoteMessage message) async {
// Update local database or perform background sync
final data = message.data;
switch (data['type']) {
case 'new_message':
await _handleNewMessage(data);
break;
case 'sync_request':
await _performBackgroundSync();
break;
default:
print('Unknown background message type: ${data['type']}');
}
}
Server-Side Push Notification Implementation
Node.js Backend with Firebase Admin SDK
typescript
import admin from 'firebase-admin';
import { getMessaging } from 'firebase-admin/messaging';
class PushNotificationService {
constructor() {
// Initialize Firebase Admin SDK
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
});
}
async sendToDevice(token: string, notification: NotificationPayload): Promise<boolean> {
try {
const message = {
token,
notification: {
title: notification.title,
body: notification.body,
imageUrl: notification.imageUrl,
},
data: notification.data || {},
android: {
priority: 'high' as const,
notification: {
sound: 'default',
clickAction: 'FLUTTER_NOTIFICATION_CLICK',
},
},
apns: {
payload: {
aps: {
badge: notification.badge || 1,
sound: 'default',
},
},
},
};
const response = await getMessaging().send(message);
console.log('Successfully sent message:', response);
return true;
} catch (error) {
console.error('Error sending message:', error);
return false;
}
}
async sendToMultipleDevices(
tokens: string[],
notification: NotificationPayload
): Promise<BatchResponse> {
try {
const message = {
notification: {
title: notification.title,
body: notification.body,
imageUrl: notification.imageUrl,
},
data: notification.data || {},
tokens,
};
const response = await getMessaging().sendMulticast(message);
console.log(`${response.successCount} messages were sent successfully`);
if (response.failureCount > 0) {
response.responses.forEach((resp, idx) => {
if (!resp.success) {
console.error(`Failed to send to token ${tokens[idx]}:`, resp.error);
}
});
}
return response;
} catch (error) {
console.error('Error sending multicast message:', error);
throw error;
}
}
async sendToTopic(topic: string, notification: NotificationPayload): Promise<boolean> {
try {
const message = {
topic,
notification: {
title: notification.title,
body: notification.body,
imageUrl: notification.imageUrl,
},
data: notification.data || {},
};
const response = await getMessaging().send(message);
console.log('Successfully sent topic message:', response);
return true;
} catch (error) {
console.error('Error sending topic message:', error);
return false;
}
}
async scheduleNotification(
token: string,
notification: NotificationPayload,
scheduledTime: Date
): Promise<void> {
const delay = scheduledTime.getTime() - Date.now();
if (delay <= 0) {
await this.sendToDevice(token, notification);
return;
}
setTimeout(async () => {
await this.sendToDevice(token, notification);
}, delay);
}
async sendPersonalizedNotifications(
userNotifications: Array<{ token: string; notification: NotificationPayload }>
): Promise<void> {
const chunks = this.chunkArray(userNotifications, 500); // FCM limit
for (const chunk of chunks) {
const promises = chunk.map(({ token, notification }) =>
this.sendToDevice(token, notification)
);
await Promise.allSettled(promises);
// Add delay between batches to respect rate limits
await this.delay(100);
}
}
private chunkArray<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
interface NotificationPayload {
title: string;
body: string;
imageUrl?: string;
badge?: number;
data?: Record<string, string>;
}
interface BatchResponse {
responses: Array<{ success: boolean; error?: any }>;
successCount: number;
failureCount: number;
}
Advanced Features
Rich Notifications with Actions
swift
// iOS - Custom notification actions
class RichNotificationSetup {
func setupNotificationCategories() {
// Message category with reply action
let replyAction = UNTextInputNotificationAction(
identifier: "REPLY_ACTION",
title: "Reply",
options: [],
textInputButtonTitle: "Send",
textInputPlaceholder: "Type your reply..."
)
let messageCategory = UNNotificationCategory(
identifier: "MESSAGE_CATEGORY",
actions: [replyAction],
intentIdentifiers: [],
options: []
)
// Friend request category
let acceptAction = UNNotificationAction(
identifier: "ACCEPT_ACTION",
title: "Accept",
options: [.foreground]
)
let declineAction = UNNotificationAction(
identifier: "DECLINE_ACTION",
title: "Decline",
options: []
)
let friendRequestCategory = UNNotificationCategory(
identifier: "FRIEND_REQUEST_CATEGORY",
actions: [acceptAction, declineAction],
intentIdentifiers: [],
options: []
)
UNUserNotificationCenter.current().setNotificationCategories([
messageCategory,
friendRequestCategory
])
}
}
Analytics and Performance Monitoring
typescript
class NotificationAnalytics {
private analytics: AnalyticsService;
constructor(analytics: AnalyticsService) {
this.analytics = analytics;
}
trackNotificationSent(
notificationId: string,
userId: string,
type: string,
channel: string
): void {
this.analytics.track('notification_sent', {
notification_id: notificationId,
user_id: userId,
type,
channel,
timestamp: new Date().toISOString(),
});
}
trackNotificationDelivered(
notificationId: string,
userId: string,
deliveryTime: number
): void {
this.analytics.track('notification_delivered', {
notification_id: notificationId,
user_id: userId,
delivery_time_ms: deliveryTime,
timestamp: new Date().toISOString(),
});
}
trackNotificationOpened(
notificationId: string,
userId: string,
openedAt: Date
): void {
this.analytics.track('notification_opened', {
notification_id: notificationId,
user_id: userId,
opened_at: openedAt.toISOString(),
});
}
async generateEngagementReport(
startDate: Date,
endDate: Date
): Promise<EngagementReport> {
const sent = await this.analytics.count('notification_sent', startDate, endDate);
const delivered = await this.analytics.count('notification_delivered', startDate, endDate);
const opened = await this.analytics.count('notification_opened', startDate, endDate);
return {
sent,
delivered,
opened,
deliveryRate: delivered / sent,
openRate: opened / delivered,
period: { startDate, endDate },
};
}
}
interface EngagementReport {
sent: number;
delivered: number;
opened: number;
deliveryRate: number;
openRate: number;
period: { startDate: Date; endDate: Date };
}
Best Practices
1. User Experience
- Request permission at the right moment
- Provide clear value proposition for notifications
- Allow granular notification preferences
- Respect user's notification settings
2. Performance Optimization
- Batch notification sends to respect rate limits
- Implement retry logic with exponential backoff
- Monitor notification delivery rates
- Clean up invalid tokens regularly
3. Security Considerations
- Validate notification payloads on the server
- Use HTTPS for all API communications
- Implement proper authentication for notification endpoints
- Sanitize user-generated content in notifications
4. Platform-Specific Guidelines
typescript
class NotificationBestPractices {
static validateNotificationContent(notification: NotificationPayload): boolean {
// Title length limits
if (notification.title.length > 50) {
console.warn('Notification title too long, will be truncated');
}
// Body length limits
if (notification.body.length > 200) {
console.warn('Notification body too long, will be truncated');
}
// Image size validation
if (notification.imageUrl && !this.isValidImageSize(notification.imageUrl)) {
console.warn('Notification image may be too large');
return false;
}
return true;
}
static optimizeForBattery(notifications: NotificationPayload[]): NotificationPayload[] {
// Group similar notifications
const grouped = this.groupSimilarNotifications(notifications);
// Create summary notifications for groups with more than 3 items
return grouped.map(group => {
if (group.length > 3) {
return this.createSummaryNotification(group);
}
return group[0];
});
}
private static createSummaryNotification(
notifications: NotificationPayload[]
): NotificationPayload {
return {
title: `${notifications.length} new messages`,
body: 'Tap to view all messages',
data: {
type: 'summary',
count: notifications.length.toString(),
items: JSON.stringify(notifications.map(n => n.data)),
},
};
}
}
Conclusion
Push notifications are a powerful tool for user engagement, but they must be implemented thoughtfully to provide value without being intrusive. Key considerations include:
- Platform-specific implementation using APNs for iOS and FCM for Android
- Rich notification features like images, actions, and interactive elements
- Server-side infrastructure for reliable message delivery and analytics
- User experience optimization through personalization and timing
- Performance monitoring to track delivery rates and engagement metrics
Success with push notifications comes from balancing technical implementation excellence with user-centric design principles.