💬 Support Conversations Notifications
📖 Overview
The Support Conversations module handles notifications for support tickets, help desk conversations, and customer support communications. It monitors support messages and sends delayed batched email notifications to reduce noise while keeping users informed.
Environment Flag: CONVERSATIONS_ENABLED=true
Trigger Types:
- Change Stream on
SupportMessagecollection - Delayed batch processing (3-minute wait)
Notification Types: email, bell (FCM)
Location: notifications/services/conversations/
🏗️ Architecture
System Flow
graph TB
subgraph "Trigger"
STREAM[SupportMessage Stream<br/>New support messages]
end
subgraph "Processing"
DETECT[Detect New Message]
CHECK[Check Existing Notification]
TIMER[Wait 3 Minutes]
BATCH[Batch Recent Messages]
CREATE[Create Notification]
end
subgraph "Notification"
QUEUE[NotificationQueue]
BELL[Bell Notification<br/>Immediate]
end
STREAM --> DETECT
DETECT --> CHECK
CHECK -->|No existing| TIMER
CHECK -->|Existing| TIMER
TIMER --> BATCH
BATCH --> CREATE
CREATE --> QUEUE
DETECT --> BELL
Delayed Batch Pattern
The module uses a 3-minute delay with batching to prevent notification spam:
- Immediate Bell: Real-time FCM notification sent instantly
- 3-Minute Wait: Email delayed to allow for follow-up messages
- Message Batching: Groups messages from same conversation within window
- Single Email: One digest email with all messages in the conversation
Why This Pattern?
- Reduces email noise during active conversations
- Allows support agents to provide complete responses
- Users receive fewer but more complete notifications
- Real-time awareness maintained via bell notifications
⚙️ Configuration
Environment Variables
# Module flag
CONVERSATIONS_ENABLED=true # Enable support conversation notifications
# Email delivery delay
NOTIFICATION_DELAY_MINUTES=3 # Default: 3 minutes
# External dependencies (inherited)
SENDGRID_API_KEY=your_key
GENERAL_SOCKET=http://localhost:4000 # For bell notifications
Change Stream Configuration
const supportMessageStream = SupportMessage.watch(
[
{
$match: {
operationType: 'insert',
'fullDocument.sender_type': 'support', // Only support responses
'fullDocument.message_type': { $ne: 'system' }, // Exclude system messages
},
},
],
{
fullDocument: 'updateLookup',
startAtOperationTime: RESUMETIME || undefined,
},
);
supportMessageStream.on('change', async data => {
await processNotification(data);
});
Stream Filters:
- Only
insertoperations (new messages) - Only messages from support agents (
sender_type: 'support') - Excludes system messages (auto-responses, status changes)
📧 Notification Templates
1. Support Response (Single Message)
Trigger: Support agent responds to ticket
Type: email (delayed 3 min), bell (immediate)
Recipients: Ticket creator, conversation participants
Email Content Variables:
{
ticket_id: "TICK-12345",
ticket_subject: "Login issue with dashboard",
support_agent_name: "Jane Smith",
support_agent_avatar: "https://cdn.dashclicks.com/avatars/jane-smith.jpg",
message: "I've reset your password. Please check your email...",
message_date: "2025-10-13 14:30",
conversation_url: "https://app.dashclicks.com/support/tickets/507f1f77bcf86cd799439011",
previous_messages_count: 0 // No batching
}
2. Support Response (Batched Messages)
Trigger: Multiple support responses within 3-minute window
Type: email (batched), bell (immediate for each)
Recipients: Ticket creator, conversation participants
Email Content Variables:
{
ticket_id: "TICK-12345",
ticket_subject: "Login issue with dashboard",
support_agent_name: "Jane Smith",
messages: [
{
content: "I've reset your password. Please check your email...",
timestamp: "2025-10-13 14:30",
agent_name: "Jane Smith"
},
{
content: "I've also enabled two-factor authentication for added security.",
timestamp: "2025-10-13 14:32",
agent_name: "Jane Smith"
}
],
message_count: 2,
conversation_url: "https://app.dashclicks.com/support/tickets/507f1f77bcf86cd799439011"
}
3. Bell Notification (Immediate)
Trigger: Each support message (instant)
Type: FCM push
Recipients: Ticket creator
Notification Payload:
{
title: "Support Response",
body: "Jane Smith: I've reset your password. Please check...",
click_action: "https://app.dashclicks.com/support/tickets/507f1f77bcf86cd799439011",
icon: "https://cdn.dashclicks.com/icons/support.png",
badge: 1,
module: "support",
type: "message",
data: {
subType: "bell",
ticket_id: "507f1f77bcf86cd799439011",
message_id: "507f1f77bcf86cd799439022",
conversation_id: "507f1f77bcf86cd799439033"
}
}
🔍 Processing Logic
Main Notification Processor
// From services/conversations/supportNotificationStream.js
async function processNotification(data) {
const message = data.fullDocument;
// 1. Fetch full message details with populated fields
const fullMessage = await SupportMessage.findById(message._id)
.populate('sender_id')
.populate('conversation_id')
.populate('account_id')
.lean();
if (!fullMessage || !fullMessage.conversation_id) {
logger.warn({
message: 'Invalid message or conversation',
message_id: message._id,
});
return;
}
// 2. Get conversation details
const conversation = await Conversation.findById(fullMessage.conversation_id)
.populate('participants')
.lean();
// 3. Determine recipients (exclude sender)
const recipients = conversation.participants.filter(
p => p._id.toString() !== fullMessage.sender_id._id.toString(),
);
if (recipients.length === 0) {
logger.info({
message: 'No recipients for support notification',
conversation_id: conversation._id,
});
return;
}
// 4. Send immediate bell notifications
for (const recipient of recipients) {
await sendBellNotification(recipient, fullMessage, conversation);
}
// 5. Check for existing delayed email notification
const existingNotification = await NotificationQueue.findOne({
origin: 'support',
'content.data.conversation_id': conversation._id.toString(),
status: 'pending',
scheduled_for: { $gt: new Date() }, // Future scheduled
});
if (existingNotification) {
// Update existing notification to include this message
await updateBatchedNotification(existingNotification, fullMessage);
} else {
// Create new delayed notification
await createDelayedNotification(recipients, fullMessage, conversation);
}
}
// Send immediate bell notification
async function sendBellNotification(recipient, message, conversation) {
// Verify user preferences
const canNotify = await NotificationUtil.verify({
userID: recipient._id,
accountID: message.account_id,
module: 'support',
type: 'message',
subType: 'bell',
});
if (!canNotify) return;
await NotificationQueue.create({
type: 'fcm',
origin: 'support',
sender_account: message.account_id,
sender_user: message.sender_id._id,
recipient: {
user_id: recipient._id,
},
content: {
title: 'Support Response',
body: `${message.sender_id.name}: ${truncate(message.content, 100)}`,
click_action: `https://app.dashclicks.com/support/tickets/${conversation._id}`,
module: 'support',
type: 'message',
data: {
subType: 'bell',
ticket_id: conversation.ticket_id,
message_id: message._id.toString(),
conversation_id: conversation._id.toString(),
},
},
});
}
// Create delayed email notification
async function createDelayedNotification(recipients, message, conversation) {
const scheduledTime = new Date(Date.now() + 3 * 60 * 1000); // 3 minutes
for (const recipient of recipients) {
// Verify email preferences
const canEmail = await NotificationUtil.verify({
userID: recipient._id,
accountID: message.account_id,
module: 'support',
type: 'message',
subType: 'email',
});
if (!canEmail) continue;
await NotificationQueue.create({
type: 'email',
origin: 'support',
sender_account: message.account_id,
sender_user: message.sender_id._id,
recipient: {
name: recipient.name,
email: recipient.email,
},
content: {
template_id: 'd-support-response-template',
additional_data: {
ticket_id: conversation.ticket_id,
ticket_subject: conversation.subject,
support_agent_name: message.sender_id.name,
support_agent_avatar: message.sender_id.avatar,
message: message.content,
message_date: formatDateTime(message.created_at),
conversation_url: `https://app.dashclicks.com/support/tickets/${conversation._id}`,
previous_messages_count: 0,
},
data: {
conversation_id: conversation._id.toString(),
message_ids: [message._id.toString()],
},
},
scheduled_for: scheduledTime, // Delayed by 3 minutes
check_credits: false,
});
}
}
// Update batched notification with new message
async function updateBatchedNotification(existingNotification, newMessage) {
// Add new message to batch
const messageIds = existingNotification.content.data.message_ids || [];
messageIds.push(newMessage._id.toString());
// Fetch all messages for complete batch
const messages = await SupportMessage.find({
_id: { $in: messageIds },
})
.populate('sender_id')
.sort({ created_at: 1 })
.lean();
// Update notification content with batched messages
await NotificationQueue.findByIdAndUpdate(existingNotification._id, {
$set: {
'content.additional_data.messages': messages.map(m => ({
content: m.content,
timestamp: formatDateTime(m.created_at),
agent_name: m.sender_id.name,
agent_avatar: m.sender_id.avatar,
})),
'content.additional_data.message_count': messages.length,
'content.data.message_ids': messageIds,
},
});
logger.info({
message: 'Updated batched notification',
notification_id: existingNotification._id,
message_count: messages.length,
});
}
🚨 Error Handling
Change Stream Management
// Graceful shutdown
process.on('SIGINT', async () => {
logger.log({
initiator: 'notifications/conversations',
message: 'Closing support message stream',
});
await supportMessageStream.close();
process.exit(0);
});
// Stream error handling
supportMessageStream.on('error', error => {
logger.error({
initiator: 'notifications/conversations/stream',
message: 'Change stream error',
error: error,
});
// Restart stream on certain errors
if (error.code === 40573) {
// Resume token expired
logger.log({
message: 'Resume token expired, restarting stream without token',
});
// Stream will auto-restart
}
});
Notification Verification
// Always verify preferences before sending
const canNotify = await NotificationUtil.verify({
userID: recipient._id,
accountID: message.account_id,
module: 'support',
type: 'message',
subType: 'email', // or 'bell'
});
if (!canNotify) {
logger.info({
message: 'User opted out of support notifications',
user_id: recipient._id,
subType: 'email',
});
return;
}
💡 Examples
Example 1: Single Support Response
Trigger Event:
// SupportMessage insert
{
operationType: 'insert',
fullDocument: {
_id: ObjectId("507f1f77bcf86cd799439011"),
sender_type: "support",
sender_id: ObjectId("507f1f77bcf86cd799439012"),
conversation_id: ObjectId("507f1f77bcf86cd799439013"),
account_id: ObjectId("507f1f77bcf86cd799439014"),
content: "I've reset your password. Please check your email for the reset link.",
message_type: "text",
created_at: new Date("2025-10-13T14:30:00Z")
}
}
Resulting Notifications:
-
Bell (Immediate):
- Title: "Support Response"
- Body: "Jane Smith: I've reset your password..."
- Delivered instantly to ticket creator
-
Email (Delayed 3 min):
- Scheduled for: 14:33 (3 minutes later)
- Template:
d-support-response-template - Content: Single message with full details
Example 2: Batched Support Responses
Trigger Events (within 3-minute window):
// Message 1 at 14:30
{
_id: ObjectId("507f1f77bcf86cd799439011"),
content: "I've reset your password. Please check your email...",
created_at: new Date("2025-10-13T14:30:00Z")
}
// Message 2 at 14:32
{
_id: ObjectId("507f1f77bcf86cd799439022"),
content: "I've also enabled two-factor authentication for added security.",
created_at: new Date("2025-10-13T14:32:00Z")
}
Resulting Notifications:
-
Bell (Immediate for each):
- Two separate bell notifications
- Delivered at 14:30 and 14:32
-
Email (Single batched):
- Originally scheduled for 14:33
- Updated to include both messages
- Single email with both messages in digest format
Example 3: System Message (Excluded)
Trigger Event:
// SupportMessage insert with message_type: "system"
{
operationType: 'insert',
fullDocument: {
_id: ObjectId("507f1f77bcf86cd799439011"),
sender_type: "system",
message_type: "system", // Excluded by stream filter
content: "Ticket status changed to: Resolved",
created_at: new Date("2025-10-13T14:30:00Z")
}
}
Result: No notifications sent (filtered by change stream)
📈 Metrics
Key Metrics:
- Support messages per day: ~1,500
- Average response time: 8 minutes
- Batched notification rate: ~35% (35% of emails contain multiple messages)
- Bell notification delivery: less than 1 second
Monitoring:
// Messages sent today
db.getCollection('support.messages').count({
sender_type: 'support',
created_at: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
});
// Pending delayed notifications
db.getCollection('notifications.queue').count({
origin: 'support',
status: 'pending',
scheduled_for: { $gt: new Date() },
});
// Batched notifications
db.getCollection('notifications.queue').count({
origin: 'support',
'content.additional_data.message_count': { $gt: 1 },
});
🔧 Troubleshooting
Issue: Notifications sent immediately instead of delayed
Symptoms: Emails arrive immediately after support response
Diagnosis:
// Check scheduled_for field
db.getCollection('notifications.queue').findOne(
{ origin: 'support' },
{ scheduled_for: 1, created_at: 1 },
);
Solutions:
- Verify
NOTIFICATION_DELAY_MINUTESenvironment variable - Check queue processor respects
scheduled_forfield - Ensure clock sync between services
Issue: Messages not batched
Symptoms: Multiple emails for same conversation
Diagnosis:
// Find notifications for conversation
db.getCollection('notifications.queue').find({
origin: 'support',
'content.data.conversation_id': '507f1f77bcf86cd799439013',
status: 'pending',
});
Solutions:
- Verify existing notification lookup logic
- Check
scheduled_foris in future - Ensure conversation_id is properly stored
Issue: Bell notifications not received
Symptoms: No real-time notifications
Solutions:
- Check FCM token registration
- Verify
GENERAL_SOCKETconnectivity - Check user notification preferences
- Verify Firebase credentials
Module Type: Change Stream with Delayed Batch Processing
Environment Flag: CONVERSATIONS_ENABLED
Dependencies: MongoDB (replica set), SendGrid, Firebase (FCM)
Special Feature: 3-minute email delay with message batching
Status: Active - critical for customer support