FCM - Notification Management
๐ Overviewโ
FCM notification management provides endpoints for users to view their notification history, mark notifications as read, and filter by module or type. This powers the notification center UI in DashClicks applications with support for pagination, unread counts, and per-notification read status.
Source Files:
- Controller:
external/Integrations/FCM/Controller/fcm.js(list,read) - Model:
external/Integrations/FCM/Model/fcm-notification.js - Routes:
external/Integrations/FCM/Routes/fcm.js
External API: N/A (database operations only)
๐๏ธ Collections Usedโ
fcm.notificationsโ
- Operations: Read, Update
- Model:
shared/models/fcm-notification.js - Usage Context: Query notification history and update read status
Document Structure:
{
"_id": ObjectId,
"account": ObjectId,
"users": [ObjectId, ObjectId], // Recipients
"type": "task_assigned",
"module": "tasks",
"message": {
"title": "New Task Assigned",
"body": "You have been assigned to Project Alpha",
"data": { /* custom payload */ }
},
"read_by": [ObjectId], // Users who marked as read
"removed_by": [ObjectId], // Users who dismissed notification
"sent_by": ObjectId, // Sender user ID
"createdAt": ISODate,
"updatedAt": ISODate
}
๐ Data Flowโ
Notification List Flowโ
sequenceDiagram
participant Client as Web/Mobile Client
participant Controller as FCM Controller
participant Model as Notification Model
participant DB as MongoDB (fcm.notifications)
Client->>Controller: GET /v1/e/fcm?page=1&limit=25&module=tasks
Controller->>Controller: Extract filters from query
Controller->>Controller: Build MongoDB query conditions
par Parallel Database Queries
Controller->>Model: list(conditions, sort, skip, limit)
Model->>DB: Find notifications
DB-->>Model: Notification documents
Controller->>Model: count(conditions)
Model->>DB: Count total
DB-->>Model: Total count
Controller->>Model: count(conditions + unread filter)
Model->>DB: Count unread
DB-->>Model: Unread count
end
Controller->>Controller: Add is_read flag to each notification
Controller->>Controller: Generate pagination metadata
Controller-->>Client: {data, pagination, unread_count}
Mark as Read Flowโ
sequenceDiagram
participant Client as Web/Mobile Client
participant Controller as FCM Controller
participant Model as Notification Model
participant DB as MongoDB (fcm.notifications)
Client->>Controller: PUT /v1/e/fcm/read?module=tasks
Controller->>Controller: Build query conditions
Controller->>Controller: Add unread filter (read_by != uid)
Controller->>Model: updateAll(conditions, {$addToSet: {read_by: uid}})
Model->>DB: Update matching documents
DB-->>Model: Update result
Controller-->>Client: {success: true, message: "SUCCESS"}
๐ง Business Logic & Functionsโ
Controller Functionsโ
list(req, res, next)โ
Purpose: Retrieve paginated notification list with filtering and unread count
Source: Controller/fcm.js
External API Endpoint: N/A (database query only)
Parameters:
req.query.page(Number, optional) - Page number (default: 0, where 0 = page 1)req.query.limit(Number, optional) - Records per page (default: 25)req.query.module(String, optional) - Filter by module (e.g., 'tasks')req.query.type(String, optional) - Filter by type (e.g., 'task_assigned')req.query.sortOrder(String, optional) - Sort direction ('asc' or 'desc')req.query.sortField(String, optional) - Field to sort byreq.auth.account_id(ObjectId) - Account ID from JWTreq.auth.uid(ObjectId) - User ID from JWT
Returns: JSON response with notifications, pagination, and unread count
{
"success": true,
"message": "SUCCESS",
"data": [
{
"id": "507f1f77bcf86cd799439011",
"type": "task_assigned",
"module": "tasks",
"message": {
"title": "New Task Assigned",
"body": "You have been assigned to Project Alpha",
"data": { /* custom payload */ }
},
"is_read": false,
"createdAt": "2023-10-01T12:00:00.000Z",
"updatedAt": "2023-10-01T12:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 25,
"totalPages": 5,
"totalResults": 123,
"unread_count": 15
}
}
Business Logic Flow:
-
Parse Query Parameters
- Extract pagination params (page, limit)
- Extract filter params (module, type)
- Extract sort params (sortField, sortOrder)
- Set defaults: limit=25, page=0
-
Calculate Skip Offset
skip = page ? (page - 1) * limit : 0- Page 0 or undefined = no skip
- Page 1 = skip 0, Page 2 = skip 25, etc.
-
Build Query Conditions
- Filter by account_id
- Filter by uid in users array
- Exclude if uid in removed_by array
- Add module filter if provided
- Add type filter if provided
-
Build Sort Criteria
- Default:
{ createdAt: 'desc' }(newest first) - Custom:
{ [sortField]: sortOrder }if provided
- Default:
-
Execute Parallel Queries
- Query 1: Fetch notification list
- Query 2: Count total matching notifications
- Query 3: Count unread notifications (read_by != uid)
-
Process Notification List
- Add
is_readboolean flag to each notification - Check if uid exists in read_by array
- Remove read_by and removed_by arrays from response
- Add
-
Build Pagination Metadata
- Calculate totalPages
- Include page, limit, totalResults
- Add unread_count from query 3
-
Return Response
- Return data array, pagination object
Query Conditions Example:
// Base conditions
{
"account": "507f1f77bcf86cd799439012",
"users": "507f1f77bcf86cd799439013",
"removed_by": { "$ne": "507f1f77bcf86cd799439013" }
}
// With filters
{
"account": "507f1f77bcf86cd799439012",
"users": "507f1f77bcf86cd799439013",
"removed_by": { "$ne": "507f1f77bcf86cd799439013" },
"module": "tasks",
"type": "task_assigned"
}
// Unread count query
{
"account": "507f1f77bcf86cd799439012",
"users": "507f1f77bcf86cd799439013",
"removed_by": { "$ne": "507f1f77bcf86cd799439013" },
"read_by": { "$ne": "507f1f77bcf86cd799439013" }
}
Request Example:
GET /v1/e/fcm?page=1&limit=25&module=tasks&sortOrder=desc&sortField=createdAt
Authorization: Bearer {jwt_token}
Success Response:
{
"success": true,
"message": "SUCCESS",
"data": [
{
"id": "507f1f77bcf86cd799439011",
"type": "task_assigned",
"module": "tasks",
"message": {
"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"
},
"is_read": false,
"metadata": {
"task": { "task_id": "task_123" }
},
"createdAt": "2023-10-01T12:00:00.000Z",
"updatedAt": "2023-10-01T12:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 25,
"totalPages": 5,
"totalResults": 123,
"unread_count": 15
}
}
Read Status Determination:
// For each notification
const isReadIndex = (notification.read_by || []).findIndex(u => u.toString() === uid.toString());
notification.is_read = isReadIndex > -1 ? true : false;
Error Handling:
- Database Errors: Propagated to error middleware via
next(error) - Invalid Parameters: No validation, defaults used
Example Usage:
// Fetch unread task notifications
const response = await fetch('/v1/e/fcm?page=1&limit=10&module=tasks', {
headers: { Authorization: `Bearer ${token}` },
});
const { data, pagination } = await response.json();
console.log(`Unread: ${pagination.unread_count}`);
Side Effects:
- โน๏ธ Database Reads: 3 separate queries (list, total count, unread count)
- โน๏ธ No Writes: Read-only operation
read(req, res, next)โ
Purpose: Mark notifications as read for authenticated user
Source: Controller/fcm.js
External API Endpoint: N/A (database update only)
Parameters:
req.query.module(String, optional) - Filter by modulereq.query.type(String, optional) - Filter by typereq.auth.account_id(ObjectId) - Account ID from JWTreq.auth.uid(ObjectId) - User ID from JWT
Returns: JSON response
{
"success": true,
"message": "SUCCESS"
}
Business Logic Flow:
-
Extract Authentication Context
- Get account_id and uid from JWT
-
Extract Filter Parameters
- Get module and type from query string
- Both optional
-
Build Query Conditions
- Filter by account
- Filter by uid in users array
- Exclude if uid in removed_by array
- Only unread: Filter by uid NOT in read_by array
- Add module filter if provided
- Add type filter if provided
-
Update Matching Notifications
- Use
$addToSetto add uid to read_by array - Atomic operation prevents duplicates
- Updates all matching documents
- Use
-
Return Success Response
- No details about number of updated documents
Query Conditions Example:
// Base conditions (mark all unread as read)
{
"account": "507f1f77bcf86cd799439012",
"users": "507f1f77bcf86cd799439013",
"removed_by": { "$ne": "507f1f77bcf86cd799439013" },
"read_by": { "$ne": "507f1f77bcf86cd799439013" }
}
// With module filter (mark all unread task notifications as read)
{
"account": "507f1f77bcf86cd799439012",
"users": "507f1f77bcf86cd799439013",
"removed_by": { "$ne": "507f1f77bcf86cd799439013" },
"read_by": { "$ne": "507f1f77bcf86cd799439013" },
"module": "tasks"
}
Update Operation:
// MongoDB update
db.fcm.notifications.updateMany(conditions, {
$addToSet: { read_by: ObjectId('507f1f77bcf86cd799439013') },
});
Request Examples:
# Mark all unread notifications as read
PUT /v1/e/fcm/read
Authorization: Bearer {jwt_token}
# Mark all unread task notifications as read
PUT /v1/e/fcm/read?module=tasks
Authorization: Bearer {jwt_token}
# Mark specific type as read
PUT /v1/e/fcm/read?module=tasks&type=task_assigned
Authorization: Bearer {jwt_token}
Success Response:
{
"success": true,
"message": "SUCCESS"
}
Error Handling:
- Database Errors: Propagated to error middleware via
next(error) - No Matching Notifications: Not treated as error, success returned
Example Usage:
// Mark all notifications as read when user opens notification center
await fetch('/v1/e/fcm/read', {
method: 'PUT',
headers: { Authorization: `Bearer ${token}` },
});
// Mark task notifications as read when user views tasks module
await fetch('/v1/e/fcm/read?module=tasks', {
method: 'PUT',
headers: { Authorization: `Bearer ${token}` },
});
Side Effects:
- โ ๏ธ Database Update: Modifies read_by array for matching notifications
- โ ๏ธ Multiple Documents: May update many notifications at once
- โน๏ธ Idempotent: Calling multiple times has same effect (uses $addToSet)
Model Functionsโ
list(conditions, sort, skip, limit)โ
Purpose: Query notifications with sorting and pagination
Source: Model/fcm-notification.js
Parameters:
conditions(Object) - MongoDB query conditionssort(Object) - Sort criteria (e.g.,{ createdAt: 'desc' })skip(Number) - Number of documents to skiplimit(Number) - Maximum documents to return
Returns: Promise<Array<Object>> - Array of notification documents
Business Logic Flow:
-
Execute Query
- Apply conditions filter
- Apply sort order
- Skip specified number of documents
- Limit result set size
-
Convert to Plain Objects
- Call
.toJSON()on each document - Applies schema transformations (removes _id, etc.)
- Call
-
Return Array
- Return plain JavaScript objects
Example Usage:
const notifications = await fcmNotificationModal.list(
{ account: 'account_123', users: 'user_456' },
{ createdAt: 'desc' },
0,
25,
);
Side Effects:
- โน๏ธ Database Read: Queries fcm.notifications collection
count(conditions)โ
Purpose: Count notifications matching conditions
Source: Model/fcm-notification.js
Parameters:
conditions(Object) - MongoDB query conditions
Returns: Promise<Number> - Count of matching documents
Example Usage:
const totalNotifications = await fcmNotificationModal.count({
account: 'account_123',
users: 'user_456',
});
const unreadCount = await fcmNotificationModal.count({
account: 'account_123',
users: 'user_456',
read_by: { $ne: 'user_456' },
});
Side Effects:
- โน๏ธ Database Read: Executes count query
updateAll(conditions, data)โ
Purpose: Update multiple notification documents
Source: Model/fcm-notification.js
Parameters:
conditions(Object) - MongoDB query conditionsdata(Object) - Update operations (e.g.,{ $addToSet: { read_by: uid } })
Returns: Promise<Boolean> - true on success
Example Usage:
await fcmNotificationModal.updateAll(
{ account: 'account_123', read_by: { $ne: 'user_456' } },
{ $addToSet: { read_by: 'user_456' } },
);
Side Effects:
- โ ๏ธ Database Update: Modifies multiple documents
๐ Integration Pointsโ
Internal Servicesโ
Notification Center UI:
- Initial Load: Fetch first page with unread count
- Pagination: Load additional pages as user scrolls
- Filter Tabs: Filter by module to show category-specific notifications
- Mark Read: Mark all or filtered notifications as read
- Real-time Updates: Poll or WebSocket to fetch new notifications
Common UI Pattern:
// Notification Center Component
class NotificationCenter {
async loadNotifications(page = 1) {
const response = await fetch(`/v1/e/fcm?page=${page}&limit=25`, {
headers: { Authorization: `Bearer ${token}` },
});
const { data, pagination } = await response.json();
// Update badge with unread count
this.updateBadge(pagination.unread_count);
// Render notifications
this.renderNotifications(data);
}
async markAllRead() {
await fetch('/v1/e/fcm/read', {
method: 'PUT',
headers: { Authorization: `Bearer ${token}` },
});
// Reload to update UI
await this.loadNotifications();
}
}
Common Filtering Patternsโ
By Module:
// Tasks tab
GET /v1/e/fcm?module=tasks&page=1&limit=25
// Orders tab
GET /v1/e/fcm?module=orders&page=1&limit=25
// All notifications
GET /v1/e/fcm?page=1&limit=25
By Type:
// Only task assignments
GET /v1/e/fcm?module=tasks&type=task_assigned&page=1&limit=25
// Only order updates
GET /v1/e/fcm?module=orders&type=order_updated&page=1&limit=25
Sorting:
// Newest first (default)
GET /v1/e/fcm?sortField=createdAt&sortOrder=desc
// Oldest first
GET /v1/e/fcm?sortField=createdAt&sortOrder=asc
๐งช Edge Cases & Special Handlingโ
Page Number Quirkโ
Issue: Page parameter uses 0-based indexing internally but 1-based in API
Implementation:
page = page ? parseInt(page) : 0; // Page 0 or undefined = first page
skip = page ? (page - 1) * limit : 0; // Page 1 = skip 0, Page 2 = skip 25
API Behavior:
?page=0โ skip 0 (first page)?page=1โ skip 0 (first page)?page=2โ skip 25 (second page)- No page param โ skip 0 (first page)
Read Status Flagโ
Issue: Need to show per-user read status without exposing all readers
Handling:
read_byarray contains all users who read notification- Controller adds
is_readboolean based on current user read_byarray removed from response for privacy
Logic:
notification.is_read = notification.read_by.includes(currentUserId);
delete notification.read_by;
Removed Notificationsโ
Issue: Users might want to dismiss/remove notifications
Current Implementation:
removed_byarray tracks users who dismissed- Notifications excluded from list if user in
removed_by - No API endpoint to add user to
removed_by(feature gap)
Future Enhancement Needed:
// Endpoint needed: PUT /v1/e/fcm/remove/:id
router.put('/remove/:id', fcmController.remove);
// Controller logic
exports.remove = async (req, res, next) => {
await fcmNotificationModal.updateOne(
{ _id: req.params.id },
{ $addToSet: { removed_by: req.auth.uid } },
);
return res.json({ success: true });
};
Unread Count Accuracyโ
Issue: Unread count must match filtered results
Handling:
- Unread count query uses same base conditions as list query
- Adds
read_by: { $ne: uid }filter - Ensures count accuracy even with module/type filters
Empty Result Setsโ
Issue: User might have no notifications
Handling:
{
"success": true,
"message": "SUCCESS",
"data": [],
"pagination": {
"page": 1,
"limit": 25,
"totalPages": 0,
"totalResults": 0,
"unread_count": 0
}
}
โ ๏ธ Important Notesโ
- ๐ Default Pagination: 25 notifications per page by default
- ๐ข Page Indexing: Page 0 and page 1 both return first page
- ๐๏ธ Privacy: read_by and removed_by arrays excluded from API response
- ๐ Idempotent: Mark as read can be called multiple times safely
- ๐ Unread Count: Calculated dynamically per query, not cached
- ๐ท๏ธ Filtering: Supports filtering by module and/or type
- ๐ Default Sort: Newest first (createdAt descending)
- ๐ซ No Deletion: Notifications never deleted, only marked as removed
- ๐ฏ User Scoped: All queries automatically scoped to authenticated user
๐ Related Documentationโ
- Integration Overview: FCM Integration
- Send Notifications: ./notifications.md
- Token Management: ./tokens.md
- MongoDB Queries: Mongoose Query Documentation