Skip to main content

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 title
  • body (String) - Notification body text
  • data (Object) - Custom data payload
  • click_action (String) - URL to open on notification click
  • tokens (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:

  1. Build Multicast Payload

    • Create data structure with tokens and notification content
    • Set priority to high for immediate delivery
    • Include custom data payload
  2. Send via Firebase SDK

    • Call admin.messaging().sendEachForMulticast()
    • Firebase handles HTTP/2 push to devices
  3. Parse Response

    • Extract success/failure counts
    • Map message IDs from responses
    • Format response to match legacy FCM API structure
  4. 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 IDs
  • req.body.notification (Object) - Notification content
    • title (String) - Notification title
    • body (String) - Notification body
    • data (Object) - Custom data payload
    • click_action (String, optional) - URL to open on click
    • module (String) - Module identifier (e.g., 'tasks', 'orders')
    • type (String) - Notification type (e.g., 'task_assigned')
  • req.auth.account_id (ObjectId) - Account ID from JWT
  • req.auth.uid (ObjectId) - User ID of sender

Returns: JSON response

{
"success": true,
"message": "SUCCESS"
}

Business Logic Flow:

  1. Extract Request Data

    • Get account_id and uid from JWT
    • Extract users array and notification object
  2. Set Default Click Action

    • If click_action not provided, use active domain
    • Calls getActiveDomain(req) utility
  3. Fetch User Tokens

    • Query fcm.tokens for all recipient users
    • Call fcmTokenModal.findAll(users, accountID)
  4. Flatten Token Arrays

    • Extract web_token arrays from all user documents
    • Combine into single array of tokens
  5. Send Notifications

    • Only if tokens found
    • Add module, type, click_action to data payload
    • Call sendWebNotification utility
    • Merge delivery response with notification content
  6. Save Notification History

    • Create document with module, type, users, message
    • Include account, sent_by fields
    • Save to fcm.notifications collection
  7. 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:

  1. Task Management: Task assignments, status changes, comments
  2. Order Management: Order updates, status changes, new orders
  3. Communications: New messages, mentions, replies
  4. Reports: Report generation complete, scheduled reports
  5. 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_action parameter 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:

  • module field identifies feature area (tasks, orders, etc.)
  • type field identifies specific event (task_assigned, order_updated)
  • Both stored in notification document and data payload

Common Modules:

  • tasks - Task management
  • orders - Order management
  • conversations - Messaging
  • reports - Report generation
  • activities - 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

๐Ÿ’ฌ

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:30 AM