FCM Push Notifications - Architecture Diagram
System Overview
Copy
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ TATU NOTIFICATION SYSTEM ┃
┃ (Three-Channel System) ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┌─────────────┐
│ EVENT │
│ OCCURS │
│ (e.g. new │
│ booking) │
└──────┬──────┘
│
↓
┌─────────────────────────┐
│ createNotification() │
│ @firebase/server/ │
│ notifications.ts │
└───────────┬─────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
↓ ↓ ↓
┌──────────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ FIRESTORE │ │ FCM PUSH │ │ EMAIL │
│ DOCUMENT │ │ NOTIFICATION │ │ (Mailgun) │
│ │ │ (NEW!) │ │ │
│ user-notif.../ │ │ │ │ Via Cloud Fns │
│ notifications/ │ │ To all user │ │ If enabled in │
│ {notifId} │ │ devices │ │ preferences │
└────────┬─────────┘ └────────┬────────┘ └────────┬─────────┘
│ │ │
↓ ↓ ↓
┌──────────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ IN-APP │ │ NATIVE OS │ │ USER'S │
│ NOTIFICATION │ │ NOTIFICATION │ │ EMAIL │
│ CENTER │ │ │ │ INBOX │
│ │ │ iOS/Android │ │ │
│ Web/Mobile │ │ Notification │ │ Gmail, etc. │
│ useNotifications│ │ Tray │ │ │
└────────┬─────────┘ └────────┬────────┘ └──────────────────┘
│ │
│ │
│ ↓
│ ┌─────────────────┐
│ │ USER TAPS │
│ │ NOTIFICATION │
│ └────────┬────────┘
│ │
│ ↓
│ ┌─────────────────┐
│ │ APP OPENS TO │
│ │ DEEP LINK │
│ │ (booking/id) │
│ └────────┬────────┘
│ │
└────────────────────┘
│
↓
┌──────────────────┐
│ NOTIFICATION │
│ MARKED AS READ │
└──────────────────┘
Token Registration Flow
Copy
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ FCM TOKEN REGISTRATION FLOW ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┌────────────────┐
│ MOBILE APP │
│ STARTS │
└───────┬────────┘
│
↓
┌────────────────┐
│ USER LOGS IN │
└───────┬────────┘
│
↓
┌─────────────────────────┐
│ Request Notification │
│ Permissions │
│ (iOS/Android) │
└───────┬─────────────────┘
│
↓
┌─────────────────────────┐
│ Get FCM Token │
│ from Device │
│ (Expo/Firebase SDK) │
└───────┬─────────────────┘
│
↓
┌─────────────────────────┐
│ useRegisterFCMToken() │
│ Hook Called │
└───────┬─────────────────┘
│
↓
┌─────────────────────────┐
│ POST /api/notifications│
│ /register-fcm-token │
│ │
│ Body: { │
│ token, │
│ deviceId, │
│ platform │
│ } │
└───────┬─────────────────┘
│
↓
┌─────────────────────────┐
│ Backend Validates │
│ & Stores in Firestore │
│ │
│ users/{userId} │
│ .fcmTokens │
│ [deviceId] = { │
│ token, │
│ platform, │
│ createdAt, │
│ lastUsed │
│ } │
└───────┬─────────────────┘
│
↓
┌─────────────────────────┐
│ ✅ Device Registered │
│ Ready to Receive │
│ Push Notifications │
└─────────────────────────┘
Notification Sending Flow (Detailed)
Copy
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ FCM NOTIFICATION SENDING FLOW ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
Event Trigger
(e.g., artist confirms booking)
│
↓
┌─────────────────────────┐
│ onBookingUpdated() │
│ Cloud Function │
└──────────┬──────────────┘
│
↓
┌─────────────────────────┐
│ createNotification() │
│ Called │
│ │
│ Parameters: │
│ - userId: "abc123" │
│ - type: BOOKING_ │
│ CONFIRMED │
│ - data: { │
│ bookingId: "xyz", │
│ artistName: "Jane" │
│ } │
└──────────┬──────────────┘
│
↓
┌─────────────────────────────────────────────────┐
│ STEP 1: Create Firestore Notification │
│ ──────────────────────────────────────── │
│ Collection: user-notifications/{userId} │
│ /notifications/{notifId} │
│ │
│ Document: { │
│ id: "notif_123", │
│ userId: "abc123", │
│ type: "BOOKING_CONFIRMED", │
│ data: { bookingId: "xyz", ... }, │
│ isRead: false, │
│ createdAt: 1234567890 │
│ } │
└──────────┬──────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────┐
│ STEP 2: Update User Notification Info │
│ ──────────────────────────────────────── │
│ Collection: user-notifications/{userId} │
│ │
│ Update: { │
│ newNotificationIds: [... + "notif_123"], │
│ lastUpdated: 1234567890 │
│ } │
└──────────┬──────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────┐
│ STEP 3: Send FCM Push (Non-Blocking) │
│ ──────────────────────────────────────── │
│ sendPushNotification() │
│ ↓ │
│ 1. Fetch user's FCM tokens from Firestore │
│ users/{userId}.fcmTokens │
│ │
│ 2. Build notification payload │
│ buildNotificationPayload() → │
│ { │
│ title: "Booking Confirmed", │
│ body: "Jane confirmed your booking", │
│ data: { bookingId, screen: "booking" } │
│ } │
│ │
│ 3. Extract all device tokens │
│ ["token1", "token2", "token3"] │
│ │
│ 4. Send to Firebase Cloud Messaging │
│ messaging().sendEachForMulticast() │
│ ├─ Device 1 (iOS) → ✅ Success │
│ ├─ Device 2 (Android) → ✅ Success │
│ └─ Device 3 (iOS) → ❌ Invalid token │
│ │
│ 5. Clean up failed tokens │
│ Remove "token3" from user document │
└─────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────┐
│ RESULT: Notification Delivered │
│ ──────────────────────────────────────── │
│ ✅ Firestore: Document created │
│ ✅ Device 1: Native notification shown │
│ ✅ Device 2: Native notification shown │
│ ❌ Device 3: Token removed (was invalid) │
└─────────────────────────────────────────────────┘
Multi-Device Support
Copy
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ONE USER, MULTIPLE DEVICES SCENARIO ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┌─────────────────┐
│ USER: John │
│ ID: "user_123" │
└────────┬────────┘
│
│ Has 3 devices:
│
┌────────────────────────┼────────────────────────┐
│ │ │
↓ ↓ ↓
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ iPhone 13 │ │ iPad Pro │ │ Android Phone │
│ │ │ │ │ │
│ Device ID: │ │ Device ID: │ │ Device ID: │
│ "iphone_xyz" │ │ "ipad_abc" │ │ "android_def" │
│ │ │ │ │ │
│ FCM Token: │ │ FCM Token: │ │ FCM Token: │
│ "token_1" │ │ "token_2" │ │ "token_3" │
│ │ │ │ │ │
│ Platform: │ │ Platform: │ │ Platform: │
│ "ios" │ │ "ios" │ │ "android" │
└───────────────┘ └───────────────┘ └───────────────┘
Stored in Firestore as:
users/user_123 {
fcmTokens: {
"iphone_xyz": {
token: "token_1",
platform: "ios",
deviceId: "iphone_xyz",
createdAt: 1234567890,
lastUsed: 1234567891
},
"ipad_abc": {
token: "token_2",
platform: "ios",
deviceId: "ipad_abc",
createdAt: 1234567892,
lastUsed: 1234567893
},
"android_def": {
token: "token_3",
platform: "android",
deviceId: "android_def",
createdAt: 1234567894,
lastUsed: 1234567895
}
}
}
When notification sent:
Notification sent to
ALL 3 devices simultaneously
│
┌──────────────┼──────────────┐
↓ ↓ ↓
Push to iPhone Push to iPad Push to Android
✅ Success ✅ Success ✅ Success
User sees notification on whichever device
they're currently using (or all of them!)
Token Lifecycle
Copy
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ FCM TOKEN LIFECYCLE ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1. TOKEN CREATION
────────────────
User logs in → App requests permissions → Device provides token
┌────────────────┐
│ Token Created │
│ "token_abc123" │
└───────┬────────┘
│
↓
┌────────────────┐
│ Registered │
│ with Backend │
└───────┬────────┘
│
↓
2. TOKEN USAGE
────────────
Notifications sent → Token used → lastUsed timestamp updated
┌────────────────┐
│ Active Token │
│ lastUsed: now │
└───────┬────────┘
│
↓
3. TOKEN EXPIRATION (Automatic Cleanup)
──────────────────
FCM send fails → Token marked invalid → Removed from Firestore
┌────────────────┐
│ Send Failed │
│ (invalid token)│
└───────┬────────┘
│
↓
┌────────────────┐
│ Auto-Cleanup │
│ Token Removed │
└───────┬────────┘
│
↓
4. TOKEN REMOVAL (Manual)
──────────────────
User logs out → App calls removeToken → Removed from Firestore
┌────────────────┐
│ User Logs Out │
└───────┬────────┘
│
↓
┌────────────────┐
│ Token Removed │
│ No more pushes │
└────────────────┘
TIMELINE:
─────────────────────────────────────────────────────────────────
Day 1: [CREATE] Token registered
Day 2: [USE] Notification sent
Day 3: [USE] Notification sent
...
Day 30: [EXPIRE] Token becomes invalid (rare)
[CLEANUP] Auto-removed on next failed send
OR
Day 15: [REMOVE] User logs out, token removed
Error Handling Flow
Copy
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ERROR HANDLING & RESILIENCE ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
Scenario 1: FCM Service Down
─────────────────────────────
createNotification()
├─ ✅ Firestore write succeeds
├─ ❌ FCM send fails
└─ ✅ Error logged, notification ID still returned
Result: In-app notification still works!
Scenario 2: Invalid Token
──────────────────────────
sendPushNotification()
├─ ✅ Fetches tokens from Firestore
├─ ❌ FCM rejects token (invalid/expired)
├─ ✅ Auto-cleanup removes bad token
└─ ✅ Other tokens still receive notification
Result: Other devices still get notification!
Scenario 3: No Tokens Registered
─────────────────────────────────
sendPushNotification()
├─ ✅ Checks for tokens
├─ ℹ️ No tokens found
└─ ✅ Logs and returns (no error)
Result: Graceful handling, no crash!
Scenario 4: Network Error
──────────────────────────
registerToken()
├─ ❌ Network request fails
├─ ❌ Hook returns error
└─ ✅ User sees error message
Result: User can retry registration!
┌─────────────────────────────────────────────────┐
│ KEY PRINCIPLE: Non-Blocking Operations │
│ ──────────────────────────────────────── │
│ FCM sending is fire-and-forget. │
│ Failures don't break core functionality. │
│ In-app notifications always work as backup. │
└─────────────────────────────────────────────────┘
Component Integration Map
Copy
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ COMPONENT RELATIONSHIPS ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
BACKEND
───────
firebase/server/
├─ notifications.ts
│ └─ createNotification() ──────────┐
│ │
└─ fcm/ │
├─ sendPushNotification.ts ◄──────┤
│ └─ Uses: buildNotificationPayload
└─ buildNotificationPayload.ts │
│
Uses │
↓ │
│
API ENDPOINTS │
───────────── │
pages/api/notifications/ │
├─ register-fcm-token.ts ◄───────────┼── Called by mobile app
├─ remove-fcm-token.ts ◄─────────────┼── Called by mobile app
└─ get-notifications.ts ◄────────────┼── In-app notifications
│
│
FRONTEND HOOKS │
────────────── │
hooks/notifications/ │
├─ useNotifications.ts │
│ └─ In-app notification center │
│ │
└─ useFCMTokenRegistration.ts ◄──────┼── Mobile app uses this
├─ useRegisterFCMToken() │
├─ useRemoveFCMToken() │
└─ useFCMTokenAutoRegistration() │
│
│
TYPES │
───── │
types/models/ │
├─ users.ts ◄─────────────────── Stores FCM tokens
│ └─ FCMToken interface │
│ └─ BaseUserInfo.fcmTokens │
│ │
└─ notifications/ ◄──────────────────┘
├─ index.ts
├─ notificationTypes.ts ──────── Notification type enum
└─ notificationConfig.tsx ─────── Display config
Benefits Visualization
Copy
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ BEFORE vs AFTER FCM IMPLEMENTATION ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
BEFORE (In-App Only)
────────────────────
User closes app
↓
Booking request comes in
↓
Notification created in Firestore
↓
❌ User doesn't know
❌ Has to open app to check
❌ Might miss time-sensitive booking
AFTER (With FCM)
────────────────
User closes app
↓
Booking request comes in
↓
Notification created in Firestore
↓
FCM push sent immediately ──────→ 📱 Device vibrates/sounds
↓
✅ User sees notification
✅ Taps to open app
✅ Goes directly to booking
✅ Can respond quickly
RELIABILITY COMPARISON
──────────────────────
Without FCM:
Polling every 5 minutes ──→ 5 minute delay
App must be open ──────→ Easy to miss
No offline queue ──────→ Notifications missed
With FCM:
Instant delivery ──────→ Real-time
Works when closed ─────→ Never miss
Automatic retry ───────→ Reliable
- ✅ Real-time push notifications
- ✅ Multi-device support
- ✅ Automatic token cleanup
- ✅ Non-blocking operations
- ✅ Graceful error handling
- ✅ Deep linking support
- ✅ Platform-specific optimizations