FCM - Send Notifications
๐ Overviewโ
FCM notification sending enables DashClicks to deliver push notifications to multiple users simultaneously using Firebase Cloud Messaging's multicast API. The system fetches user device tokens, sends notifications to all registered devices, and tracks delivery status in MongoDB.
Source Files:
- Utility:
external/Integrations/FCM/Utils/send-notification.js - Controller:
external/Integrations/FCM/Controller/fcm.js(send) - Model:
Model/fcm.js,Model/fcm-notification.js - Routes:
Routes/fcm.js
External API: Firebase Admin SDK messaging().sendEachForMulticast()
๐๏ธ Collections Usedโ
fcm.tokensโ
- Operations: Read
- Model:
shared/models/fcm-token.js - Usage Context: Fetch registered device tokens for notification recipients
fcm.notificationsโ
- Operations: Create
- Model:
shared/models/fcm-notification.js - Usage Context: Store notification history and delivery status
Saved Notification Document:
{
"_id": ObjectId,
"account": ObjectId("507f1f77bcf86cd799439012"),
"users": [
ObjectId("507f1f77bcf86cd799439013"),
ObjectId("507f1f77bcf86cd799439014")
],
"type": "task_assigned",
"module": "tasks",
"message": {
"multicast_id": 0,
"success": 2,
"failure": 0,
"results": [
{ "message_id": "0:1633036800000000%abc123" },
{ "message_id": "0:1633036801000000%def456" }
],
"title": "New Task Assigned",
"body": "You have been assigned to Project Alpha",
"data": {
"task_id": "task_123",
"module": "tasks",
"type": "task_assigned",
"click_action": "https://app.dashclicks.com/tasks/123"
},
"click_action": "https://app.dashclicks.com/tasks/123"
},
"sent_by": ObjectId("507f1f77bcf86cd799439015"),
"read_by": [],
"removed_by": [],
"metadata": {
"task": { "task_id": "task_123" }
},
"createdAt": ISODate("2023-10-01T12:00:00Z"),
"updatedAt": ISODate("2023-10-01T12:00:00Z")
}
๐ Data Flowโ
Notification Sending Flowโ
sequenceDiagram
participant Service as Internal Service
participant Controller as FCM Controller
participant TokenModel as Token Model
participant TokenDB as MongoDB (fcm.tokens)
participant Utility as Send Notification Util
participant Firebase as Firebase Admin SDK
participant Devices as User Devices
participant NotifModel as Notification Model
participant NotifDB as MongoDB (fcm.notifications)
Service->>Controller: POST /v1/e/fcm/send {users, notification}
Controller->>Controller: Extract account_id, uid from JWT
Controller->>Controller: Set default click_action from domain
Controller->>TokenModel: findAll(users, account_id)
TokenModel->>TokenDB: Find tokens for users
TokenDB-->>TokenModel: Return token documents
TokenModel-->>Controller: Array of token documents
Controller->>Controller: Flatten web_token arrays
alt Tokens found
Controller->>Utility: sendWebNotification(title, body, data, tokens)
Utility->>Firebase: sendEachForMulticast(dataToSend)
Firebase->>Devices: Push notifications via HTTP/2
Firebase-->>Utility: Delivery response
Utility-->>Controller: {success, failure, results}
Controller->>NotifModel: saveToken(notification data)
NotifModel->>NotifDB: Save notification document
else No tokens
Controller->>Controller: Skip notification sending
end
Controller-->>Service: {success: true, message: "SUCCESS"}
๐ง Business Logic & Functionsโ
Utility Functionsโ
sendWebNotification(title, body, data, click_action, tokens)โ
Purpose: Send multicast push notification via Firebase Admin SDK
Source: Utils/send-notification.js
External API Endpoint: Firebase Admin SDK messaging().sendEachForMulticast()
Parameters:
title(String) - Notification titlebody(String) - Notification body textdata(Object) - Custom data payloadclick_action(String) - URL to open on notification clicktokens(Array<String>) - Array of FCM device tokens
Returns: Promise<Object> - Delivery response
{
"multicast_id": 0,
"canolical_ids": 0,
"success": 2, // Number of successful deliveries
"failure": 0, // Number of failed deliveries
"results": [
{ "message_id": "0:1633036800000000%abc123" },
{ "message_id": "0:1633036801000000%def456" }
]
}
Business Logic Flow:
-
Build Multicast Payload
- Create data structure with tokens and notification content
- Set priority to
highfor immediate delivery - Include custom data payload
-
Send via Firebase SDK
- Call
admin.messaging().sendEachForMulticast() - Firebase handles HTTP/2 push to devices
- Call
-
Parse Response
- Extract success/failure counts
- Map message IDs from responses
- Format response to match legacy FCM API structure
-
Return Delivery Status
- Return success/failure counts and message IDs
Firebase Payload Structure:
{
"tokens": [
"fcm_token_1",
"fcm_token_2"
],
"priority": "high",
"data": {
"data": { /* custom payload */ },
"title": "Notification Title",
"body": "Notification Body",
"click_action": "https://app.dashclicks.com/..."
}
}
Firebase Response Example:
{
"successCount": 2,
"failureCount": 0,
"responses": [
{
"success": true,
"messageId": "0:1633036800000000%abc123"
},
{
"success": true,
"messageId": "0:1633036801000000%def456"
}
]
}
Error Handling:
- Invalid Tokens: Firebase marks as failed but doesn't throw error
- Network Errors: Propagated to caller
- Quota Exceeded: Firebase error propagated
- Invalid Credentials: Application initialization fails (app won't start)
Example Usage:
const response = await sendWebNotification(
'New Task Assigned',
'You have been assigned to Project Alpha',
{ task_id: 'task_123', module: 'tasks' },
'https://app.dashclicks.com/tasks/123',
['token1', 'token2', 'token3'],
);
console.log(`Delivered to ${response.success} devices`);
console.log(`Failed: ${response.failure}`);
Provider API Rate Limits:
- Firebase enforces quotas based on project tier
- Free tier: 10,000 messages/day
- Blaze (pay-as-you-go): No daily limit, per-message cost
- Maximum 500 tokens per multicast batch
Side Effects:
- โ ๏ธ External API Call: Sends push notifications via Firebase
- โ ๏ธ User Device Action: Notifications appear on user devices
- ๐ฐ Potential Cost: May incur Firebase messaging charges
Controller Functionsโ
send(req, res, next)โ
Purpose: Orchestrate notification sending - fetch tokens, send notifications, save history
Source: Controller/fcm.js
External API Endpoint: Calls sendWebNotification utility
Parameters:
req.body.users(Array<String>) - Array of recipient user IDsreq.body.notification(Object) - Notification contenttitle(String) - Notification titlebody(String) - Notification bodydata(Object) - Custom data payloadclick_action(String, optional) - URL to open on clickmodule(String) - Module identifier (e.g., 'tasks', 'orders')type(String) - Notification type (e.g., 'task_assigned')
req.auth.account_id(ObjectId) - Account ID from JWTreq.auth.uid(ObjectId) - User ID of sender
Returns: JSON response
{
"success": true,
"message": "SUCCESS"
}
Business Logic Flow:
-
Extract Request Data
- Get account_id and uid from JWT
- Extract users array and notification object
-
Set Default Click Action
- If click_action not provided, use active domain
- Calls
getActiveDomain(req)utility
-
Fetch User Tokens
- Query fcm.tokens for all recipient users
- Call
fcmTokenModal.findAll(users, accountID)
-
Flatten Token Arrays
- Extract web_token arrays from all user documents
- Combine into single array of tokens
-
Send Notifications
- Only if tokens found
- Add module, type, click_action to data payload
- Call
sendWebNotificationutility - Merge delivery response with notification content
-
Save Notification History
- Create document with module, type, users, message
- Include account, sent_by fields
- Save to fcm.notifications collection
-
Return Success Response
- Always return success (even if no tokens)
Request Example:
POST /v1/e/fcm/send
Authorization: Bearer {jwt_token}
Content-Type: application/json
{
"users": ["user_123", "user_456"],
"notification": {
"title": "New Task Assigned",
"body": "You have been assigned to Project Alpha",
"data": {
"task_id": "task_123",
"project_id": "project_789"
},
"click_action": "https://app.dashclicks.com/tasks/123",
"module": "tasks",
"type": "task_assigned"
}
}
Success Response:
{
"success": true,
"message": "SUCCESS"
}
Data Payload Enrichment:
// Original data from request
data = {
task_id: 'task_123',
project_id: 'project_789',
};
// After enrichment
data = {
task_id: 'task_123',
project_id: 'project_789',
module: 'tasks', // Added
type: 'task_assigned', // Added
click_action: 'https://...', // Added
};
Error Handling:
- No Tokens Found: Not treated as error, success response returned
- Firebase Errors: Propagated to error middleware via
next(error) - Database Errors: Propagated to error middleware
Example Usage (from internal service):
// Send notification when task is assigned
async function notifyTaskAssignment(taskId, assignedUsers) {
await fetch('/v1/e/fcm/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${serviceToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
users: assignedUsers,
notification: {
title: 'New Task Assigned',
body: `You have been assigned to task ${taskId}`,
data: { task_id: taskId },
module: 'tasks',
type: 'task_assigned',
},
}),
});
}
Side Effects:
- โ ๏ธ External API Call: Sends push notifications via Firebase
- โ ๏ธ Database Write: Saves notification history
- โ ๏ธ User Notification: Users receive push notifications on devices
- ๐ฐ Potential Cost: Firebase messaging charges may apply
๐ Integration Pointsโ
Internal Servicesโ
Notification Trigger Points:
- Task Management: Task assignments, status changes, comments
- Order Management: Order updates, status changes, new orders
- Communications: New messages, mentions, replies
- Reports: Report generation complete, scheduled reports
- Activities: Activity assignments, reminders, completions
Common Integration Pattern:
// Example: Notify users about new message
async function notifyNewMessage(conversationId, messageId, recipientIds) {
const notification = {
title: 'New Message',
body: 'You have a new message',
data: {
conversation_id: conversationId,
message_id: messageId,
},
click_action: `https://app.dashclicks.com/conversations/${conversationId}`,
module: 'conversations',
type: 'new_message',
};
await fetch('/v1/e/fcm/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
users: recipientIds,
notification,
}),
});
}
External API Dependenciesโ
Provider: Google Firebase
Service: Firebase Cloud Messaging (FCM)
SDK: Firebase Admin SDK for Node.js
API Method: admin.messaging().sendEachForMulticast()
Authentication: Service Account credentials (auto-loaded via GOOGLE_APPLICATION_CREDENTIALS)
Notification Delivery:
- Protocol: HTTP/2
- Priority: High (immediate delivery)
- TTL: Default (4 weeks for Android, varies for iOS)
- Batching: Up to 500 tokens per request
๐งช Edge Cases & Special Handlingโ
No Registered Tokensโ
Issue: Users might not have registered any FCM tokens
Handling:
if (tokens && tokens.length) {
// Send notification
} else {
// Skip sending, return success anyway
}
- Controller returns success even when no tokens found
- Notification not saved to history if no tokens
- No error thrown
Token Array Flatteningโ
Issue: Each user may have multiple tokens, need to flatten arrays
Implementation:
let tokens = [];
for (const tokenDoc of fcmTokens) {
tokens = [...tokens, ...tokenDoc.web_token];
}
- Combines all tokens from all users into single array
- Supports sending to multiple devices per user
- Each token receives separate notification
Partial Delivery Failuresโ
Issue: Some tokens may be invalid/expired while others are valid
Handling:
- Firebase returns per-token delivery status
- Success/failure counts tracked in message object
- No automatic retry for failed deliveries
- Invalid tokens not removed from database
Example Response:
{
"success": 3, // 3 devices received notification
"failure": 2, // 2 tokens invalid/failed
"results": [
{ "message_id": "..." }, // Success
{ "message_id": "..." }, // Success
{ "error": "..." }, // Failed
{ "message_id": "..." }, // Success
{ "error": "..." } // Failed
]
}
Large Recipient Listsโ
Issue: Firebase limits multicast to 500 tokens per request
Current Implementation: No batching logic
Limitation: Requests with >500 total tokens will fail
Future Enhancement Needed:
// Example batching logic (not implemented)
const BATCH_SIZE = 500;
for (let i = 0; i < tokens.length; i += BATCH_SIZE) {
const batch = tokens.slice(i, i + BATCH_SIZE);
await sendWebNotification(title, body, data, click_action, batch);
}
Click Action URLโ
Issue: Notifications should deep link to relevant page
Handling:
click_actionparameter specifies URL- If not provided, defaults to active domain
- Clients handle URL navigation on click
URL Examples:
// Specific task
click_action: 'https://app.dashclicks.com/tasks/123';
// Conversation thread
click_action: 'https://app.dashclicks.com/conversations/456';
// General dashboard
click_action: 'https://app.dashclicks.com/dashboard';
Module and Type Categorizationโ
Issue: Need to categorize notifications for filtering
Implementation:
modulefield identifies feature area (tasks, orders, etc.)typefield identifies specific event (task_assigned, order_updated)- Both stored in notification document and data payload
Common Modules:
tasks- Task managementorders- Order managementconversations- Messagingreports- Report generationactivities- Activity tracking
Notification Historyโ
Issue: Need to track sent notifications for user notification center
Handling:
- All sent notifications saved to fcm.notifications
- Includes delivery status (success/failure counts)
- Can be queried later for notification list
- No TTL - stored indefinitely
โ ๏ธ Important Notesโ
- ๐ฏ Multicast Limit: Maximum 500 tokens per request (no batching implemented)
- ๐ Partial Failures: Some devices may receive notification while others fail
- ๐พ History Tracking: All notifications saved to database regardless of delivery status
- ๐ No Tokens = Success: Returning success even when no tokens found
- ๐ Click Actions: URLs should be absolute paths to ensure proper navigation
- ๐ท๏ธ Categorization: module and type fields enable notification filtering
- ๐ Data Payload: Custom data can be accessed by client-side FCM handler
- ๐ Authentication: Requires valid JWT token with account_id and uid
- โก High Priority: All notifications sent with high priority for immediate delivery
๐ Related Documentationโ
- Integration Overview: FCM Integration
- Token Management: ./tokens.md
- Notification Management: ./notification-management.md
- Firebase Multicast: FCM Send to Multiple Devices
- Firebase Admin SDK: Admin SDK Messaging