Skip to main content

Firebase Cloud Messaging (FCM)

Firebase Cloud Messaging (FCM) is Google's cross-platform messaging solution. BookWish uses FCM to send push notifications to mobile apps (iOS and Android).

Overview

BookWish uses Firebase for:

  • Push Notifications: Send notifications to user devices
  • Cross-Platform: Unified API for iOS, Android, and web
  • Token Management: Register and manage device tokens
  • Batch Messaging: Send to multiple devices simultaneously
  • Badge Counts: Update app badge numbers (iOS)
  • Rich Notifications: Titles, bodies, and custom data

Implementation

Backend Location: /backend/src/integrations/firebase.ts Service Location: /backend/src/services/push.service.ts

Configuration

Required environment variables:

FIREBASE_PROJECT_ID=your-project-id
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..."
FIREBASE_CLIENT_EMAIL=firebase-adminsdk@your-project.iam.gserviceaccount.com

The backend uses Firebase Admin SDK with service account credentials.

Features

1. Send to Single Device

Send a notification to a specific device token.

Send Notification

import { sendToDevice } from '../integrations/firebase';

await sendToDevice('device_token_...', {
notification: {
title: 'New Wishlist Match!',
body: 'The Great Gatsby is now available at Main Street Books'
},
data: {
type: 'wishlist_match',
bookId: 'book_123',
storeId: 'store_456'
},
apns: {
payload: {
aps: {
badge: 5 // iOS badge count
}
}
}
});

Notification Payload

interface FirebasePayload {
notification: {
title: string; // Notification title
body: string; // Notification body
};
data?: Record<string, string>; // Custom key-value pairs
apns?: {
payload: {
aps: {
badge?: number; // iOS badge count
}
}
};
}

2. Send to Multiple Devices (Batch)

Send the same notification to multiple device tokens.

Batch Send

import { sendToDevices } from '../integrations/firebase';

const tokens = [
'device_token_1',
'device_token_2',
'device_token_3',
// ... more tokens
];

const result = await sendToDevices(tokens, {
notification: {
title: 'Daily Reading Challenge',
body: 'Complete your reading challenge today!'
},
data: {
type: 'challenge_reminder',
challengeId: 'challenge_789'
}
});

console.log('Success:', result.successCount);
console.log('Failures:', result.failureCount);
console.log('Invalid tokens:', result.invalidTokens);

3. Handle Invalid Tokens

FCM automatically detects invalid or expired tokens.

Invalid Token Handling

try {
await sendToDevice(token, payload);
} catch (error) {
if (error instanceof InvalidTokenError) {
// Token is invalid or unregistered
// Remove from database
await unregisterToken(error.token);
}
}

The integration automatically collects invalid tokens in batch sends:

const result = await sendToDevices(tokens, payload);

if (result.invalidTokens.length > 0) {
// These tokens should be removed from database
await removeInvalidTokens(result.invalidTokens);
}

Push Service Layer

The push service (/backend/src/services/push.service.ts) provides higher-level functions.

Register Token

Register a device token for a user:

import { registerToken } from '../services/push.service';

await registerToken(
'user_123',
'device_token_...',
'ios' // 'ios' | 'android' | 'web'
);

Send to User

Send notification to all devices registered for a user:

import { sendPush } from '../services/push.service';

await sendPush('user_123', {
title: 'Order Shipped',
body: 'Your order #1234 has been shipped!',
data: {
type: 'order_update',
orderId: 'order_1234'
},
badge: 3
});

The service automatically:

  • Fetches all tokens for the user
  • Sends to all devices
  • Removes invalid tokens

Send to Multiple Users

Send notification to multiple users:

import { sendPushBatch } from '../services/push.service';

const userIds = ['user_1', 'user_2', 'user_3'];

await sendPushBatch(userIds, {
title: 'New Feature Available',
body: 'Check out the new reading challenges!'
});

Unregister Token

Remove a device token (e.g., on logout):

import { unregisterToken } from '../services/push.service';

await unregisterToken('device_token_...');

Remove All User Tokens

Remove all tokens for a user (e.g., account deletion):

import { removeAllUserTokens } from '../services/push.service';

await removeAllUserTokens('user_123');

Database Schema

PushToken Table

CREATE TABLE push_tokens (
token VARCHAR PRIMARY KEY, -- Device token
user_id UUID NOT NULL, -- User who owns the device
platform VARCHAR NOT NULL, -- 'ios', 'android', 'web'
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX idx_push_tokens_user_id ON push_tokens(user_id);

Mobile Client Setup

iOS Setup

File: /app/ios/Runner/GoogleService-Info.plist

  1. Download GoogleService-Info.plist from Firebase Console
  2. Add to Xcode project
  3. Configure FCM in AppDelegate
import Firebase
import FirebaseMessaging

@main
class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Initialize Firebase
FirebaseApp.configure()

// Request notification permissions
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
if granted {
DispatchQueue.main.async {
application.registerForRemoteNotifications()
}
}
}

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

// Handle token registration
override func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Messaging.messaging().apnsToken = deviceToken
}
}

Android Setup

File: /app/android/app/google-services.json

  1. Download google-services.json from Firebase Console
  2. Place in android/app/ directory
  3. Firebase auto-initializes on Android
// No additional setup needed for basic FCM
// Token handling in Flutter code

Flutter Integration

Get FCM token in Flutter app:

import 'package:firebase_messaging/firebase_messaging.dart';

Future<void> setupPushNotifications() async {
final messaging = FirebaseMessaging.instance;

// Request permissions (iOS)
await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);

// Get FCM token
final token = await messaging.getToken();

if (token != null) {
// Register with backend
await api.registerPushToken(token, Platform.isIOS ? 'ios' : 'android');
}

// Listen for token refresh
messaging.onTokenRefresh.listen((newToken) {
api.registerPushToken(newToken, Platform.isIOS ? 'ios' : 'android');
});

// Handle foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
// Show in-app notification
});

// Handle background/terminated messages
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
// Navigate to relevant screen
});
}

Notification Types

Common notification types in BookWish:

Wishlist Matches

{
title: 'Wishlist Match Found!',
body: '{book_title} is now available',
data: {
type: 'wishlist_match',
bookId: '...',
storeId: '...'
}
}

Order Updates

{
title: 'Order Update',
body: 'Your order has shipped',
data: {
type: 'order_shipped',
orderId: '...'
}
}

Social Interactions

{
title: 'New Follower',
body: '{username} started following you',
data: {
type: 'new_follower',
userId: '...'
}
}

Reading Challenges

{
title: 'Challenge Update',
body: 'Complete your reading goal today!',
data: {
type: 'challenge_reminder',
challengeId: '...'
}
}

Error Handling

Common Errors

// Invalid token
if (error.code === 'messaging/invalid-registration-token') {
// Token is invalid, remove from database
}

// Token not registered
if (error.code === 'messaging/registration-token-not-registered') {
// Device uninstalled app or token expired
}

// Message too large
if (error.code === 'messaging/invalid-payload') {
// Payload exceeds 4KB limit
}

Error Codes

  • messaging/invalid-registration-token: Invalid token format
  • messaging/registration-token-not-registered: Token expired/uninstalled
  • messaging/invalid-payload: Message exceeds size limits
  • messaging/server-unavailable: FCM service temporarily down
  • messaging/internal-error: Internal FCM error

Best Practices

  1. Token Lifecycle: Update tokens when they refresh
  2. Graceful Degradation: Handle FCM unavailability gracefully
  3. Invalid Token Cleanup: Remove invalid tokens promptly
  4. Payload Size: Keep messages under 4KB
  5. Data-Only Messages: Use data messages for silent updates
  6. Badge Management: Always update badge counts on iOS
  7. Testing: Test notifications on both iOS and Android
  8. Personalization: Include user-specific data in messages

Payload Limits

FCM enforces payload size limits:

  • Maximum payload size: 4KB (4096 bytes)
  • Notification fields count against limit
  • Data fields count against limit
  • Keep messages concise

Testing

Test Notifications

Send test notification from Firebase Console:

  1. Go to Cloud Messaging
  2. Click "Send test message"
  3. Enter device token
  4. Send

Test with FCM API

# Send test notification via curl
curl -X POST https://fcm.googleapis.com/v1/projects/YOUR_PROJECT/messages:send \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": {
"token": "DEVICE_TOKEN",
"notification": {
"title": "Test",
"body": "This is a test notification"
}
}
}'

Debug Logging

Enable Firebase debug logging:

// Enable verbose logging
logger.level = 'debug';

// Logs include:
// - firebase.push_sent
// - firebase.batch_sent
// - firebase.push_failed

Rate Limits

FCM has generous rate limits:

  • Fanout: 1,000,000 messages per second
  • Burst: 10,000 messages per second to single device
  • No daily quota

iOS-Specific Considerations

APNs Configuration

FCM uses Apple Push Notification service (APNs) for iOS. Configure in Firebase Console:

  1. Upload APNs authentication key or certificate
  2. Configure team ID and bundle ID
  3. Enable production/development environments

Badge Management

Update badge count with every notification:

apns: {
payload: {
aps: {
badge: badgeCount // New badge number
}
}
}

Silent Notifications

Send silent notifications (no alert):

{
data: {
type: 'silent_sync',
action: 'refresh_feed'
},
apns: {
headers: {
'apns-priority': '5' // Low priority
},
payload: {
aps: {
contentAvailable: true
}
}
}
}

Android-Specific Considerations

Notification Channels

Android requires notification channels (handled by Flutter plugin automatically):

const AndroidNotificationChannel channel = AndroidNotificationChannel(
'bookwish_notifications',
'BookWish Notifications',
description: 'Notifications from BookWish',
importance: Importance.high,
);

Data-Only Messages

Android handles data-only messages in background:

{
data: {
type: 'sync',
action: 'update_inventory'
}
// No notification field = silent
}

Monitoring

Track notification metrics:

// Log successful sends
logger.info('push.sent', {
userId,
notificationType: payload.data?.type,
deviceCount: tokens.length
});

// Log failures
logger.error('push.failed', {
userId,
error: error.message,
invalidTokenCount: result.invalidTokens.length
});

Important metrics:

  • Success rate
  • Invalid token rate
  • Delivery latency
  • Error types

Additional Resources