Skip to main content

💬 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 SupportMessage collection
  • 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:

  1. Immediate Bell: Real-time FCM notification sent instantly
  2. 3-Minute Wait: Email delayed to allow for follow-up messages
  3. Message Batching: Groups messages from same conversation within window
  4. 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 insert operations (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:

  1. Bell (Immediate):

    • Title: "Support Response"
    • Body: "Jane Smith: I've reset your password..."
    • Delivered instantly to ticket creator
  2. 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:

  1. Bell (Immediate for each):

    • Two separate bell notifications
    • Delivered at 14:30 and 14:32
  2. 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_MINUTES environment variable
  • Check queue processor respects scheduled_for field
  • 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_for is in future
  • Ensure conversation_id is properly stored

Issue: Bell notifications not received

Symptoms: No real-time notifications

Solutions:

  1. Check FCM token registration
  2. Verify GENERAL_SOCKET connectivity
  3. Check user notification preferences
  4. 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

💬

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:31 AM