Skip to main content

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 by
  • req.auth.account_id (ObjectId) - Account ID from JWT
  • req.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:

  1. Parse Query Parameters

    • Extract pagination params (page, limit)
    • Extract filter params (module, type)
    • Extract sort params (sortField, sortOrder)
    • Set defaults: limit=25, page=0
  2. Calculate Skip Offset

    • skip = page ? (page - 1) * limit : 0
    • Page 0 or undefined = no skip
    • Page 1 = skip 0, Page 2 = skip 25, etc.
  3. 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
  4. Build Sort Criteria

    • Default: { createdAt: 'desc' } (newest first)
    • Custom: { [sortField]: sortOrder } if provided
  5. Execute Parallel Queries

    • Query 1: Fetch notification list
    • Query 2: Count total matching notifications
    • Query 3: Count unread notifications (read_by != uid)
  6. Process Notification List

    • Add is_read boolean flag to each notification
    • Check if uid exists in read_by array
    • Remove read_by and removed_by arrays from response
  7. Build Pagination Metadata

    • Calculate totalPages
    • Include page, limit, totalResults
    • Add unread_count from query 3
  8. 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 module
  • req.query.type (String, optional) - Filter by type
  • req.auth.account_id (ObjectId) - Account ID from JWT
  • req.auth.uid (ObjectId) - User ID from JWT

Returns: JSON response

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

Business Logic Flow:

  1. Extract Authentication Context

    • Get account_id and uid from JWT
  2. Extract Filter Parameters

    • Get module and type from query string
    • Both optional
  3. 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
  4. Update Matching Notifications

    • Use $addToSet to add uid to read_by array
    • Atomic operation prevents duplicates
    • Updates all matching documents
  5. 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 conditions
  • sort (Object) - Sort criteria (e.g., { createdAt: 'desc' })
  • skip (Number) - Number of documents to skip
  • limit (Number) - Maximum documents to return

Returns: Promise<Array<Object>> - Array of notification documents

Business Logic Flow:

  1. Execute Query

    • Apply conditions filter
    • Apply sort order
    • Skip specified number of documents
    • Limit result set size
  2. Convert to Plain Objects

    • Call .toJSON() on each document
    • Applies schema transformations (removes _id, etc.)
  3. 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 conditions
  • data (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:

  1. Initial Load: Fetch first page with unread count
  2. Pagination: Load additional pages as user scrolls
  3. Filter Tabs: Filter by module to show category-specific notifications
  4. Mark Read: Mark all or filtered notifications as read
  5. 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_by array contains all users who read notification
  • Controller adds is_read boolean based on current user
  • read_by array 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_by array 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

๐Ÿ’ฌ

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