servers/notifications � @leadmetrics/server-notifications
A dedicated Node.js background service that dispatches all outbound notifications across five channels: email � WhatsApp � Telegram � SMS � web (in-app). It runs as a standalone process, consumes jobs from BullMQ queues, and routes each job to the appropriate provider package.
Source: apps/servers/notifications/
Package: @leadmetrics/server-notifications
Why a Separate Service
| Concern | Reason |
|---|---|
| Isolation | Provider failures never affect the API or agent workers |
| Independent scaling | High-volume email batches scale separately from agent workers |
| Provider swaps | Changing from SendGrid to SES only touches this service |
| Rate limiting | Per-queue concurrency limits enforce provider rate limits |
| Dev filtering | Development-mode recipient allow-lists live in src/dev-filter.ts |
System Context
Platform services (API, BullMQ cron jobs, agent workers)
|
| enqueueNotification(...) <-- @leadmetrics/queue
v
Redis (BullMQ)
|-- notifications__email <- EmailHandler (concurrency 10)
|-- notifications__whatsapp <- WhatsAppHandler (concurrency 3)
|-- notifications__telegram <- TelegramHandler (concurrency 5)
|-- notifications__sms <- SmsHandler (concurrency 2, stub)
+-- notifications__web <- WebHandler (concurrency 20)
|
v
apps/servers/notifications (this service)
|-- EmailHandler -> resolveEmailProvider() -> SendGrid / SMTP / SES
|-- WhatsAppHandler -> resolveWhatsAppProvider() -> Meta Graph API
|-- TelegramHandler -> resolveTelegramProvider() -> Telegram Bot API
|-- SmsHandler -> stub (awaiting provider-msg91 / provider-twilio)
+-- WebHandler -> notification DB table + optional SSE push hubapps/servers/notifications is the only process that calls external notification
provider APIs. All other services enqueue jobs and never call providers directly.
Queue Names
BullMQ v5 does not allow
:in queue names or job IDs. The platform uses__(double-underscore) as separator throughout.
| Channel | Queue name | Concurrency | Attempts | Backoff |
|---|---|---|---|---|
notifications__email | 10 | 3 | exponential 5 s | |
notifications__whatsapp | 3 | 3 | exponential 5 s | |
| Telegram | notifications__telegram | 5 | 3 | exponential 5 s |
| SMS | notifications__sms | 2 | 3 | exponential 5 s |
| Web (in-app) | notifications__web | 20 | 3 | exponential 5 s |
Enqueueing Jobs
All platform code calls enqueueNotification() from @leadmetrics/queue:
import { enqueueNotification } from "@leadmetrics/queue";
await enqueueNotification({
tenantId: "clxxx...",
channel: "email",
type: "welcome",
templateSlug: "welcome",
priority: "high",
recipients: [{ userId, name: "John Smith", email: "john@acme.com" }],
variables: { firstName: "John", companyName: "Acme", dashboardUrl: "https://..." },
dedupeKey: `welcome__${tenantId}`, // prevents duplicate on re-enqueue
});enqueueNotification() in packages/queue/src/queues.ts:
- Queue name:
notifications__${channel} - Sanitises
:?__indedupeKey(BullMQ v5 restriction) jobId= sanitiseddedupeKey, or auto-generatednotif__<tenantId>__<type>__<nanoid>- Priority: high=1, normal=5, low=10
- Closes the queue connection after adding (stateless fire-and-forget)
Notification Job Schema
Defined in packages/queue/src/types.ts as NotificationJobData.
Mirrored as NotificationJob in apps/servers/notifications/src/types.ts.
interface NotificationJobData {
id: string; // BullMQ jobId � used for deduplication
tenantId: string;
channel: "email" | "sms" | "whatsapp" | "telegram" | "web";
type: string; // NotificationType � drives template selection
priority: "high" | "normal" | "low";
recipients: Array<{
userId?: string; // platform user ID
name: string;
email?: string; // required for email
phone?: string; // E.164 � required for sms / whatsapp
chatId?: string | number; // required for telegram
locale?: string; // e.g. "en", "ta"
}>;
templateSlug: string; // DB email_template.slug or template map key
variables: Record<string, string>; // Handlebars {{variable}} context
dedupeKey?: string;
metadata?: Record<string, string>; // arbitrary logging context (refId, refType, ...)
}Notification Types
| Type | Channels | Key variables |
|---|---|---|
| Onboarding | ||
welcome | firstName, companyName, plan, dashboardUrl, currentYear | |
| Workflow & HITL | ||
approval_required | Email + Web | activityTitle, activityRef, reviewUrl |
approval_expiring_soon | Email + Web | activityTitle, expiresAt, reviewUrl |
approval_approved | Web | activityTitle, reviewerName |
approval_rejected | Email + Web | activityTitle, rejectionNote, reviewUrl |
pipeline_preview_ready | Email + Web | month, activityCount, estimatedCredits, reviewUrl |
pipeline_blocked | Email + Web + SMS | blockedTaskTitle, failureReason, pipelineUrl |
activity_failed | Email + Web | activityTitle, activityRef, errorSummary |
| Credits & Billing | ||
credits_warning_80 | Email + Web | creditsUsed, creditsTotal, creditsRemaining, resetDate |
credits_exhausted | Email + Web + SMS | pausedActivities, topupUrl, resetDate |
credits_topup_success | Email + Web | creditsAdded, newBalance, expiresAt |
payment_failed | Email + SMS | amountInr, retryDate, billingUrl |
subscription_renewed | plan, amountInr, nextBillingDate | |
plan_upgraded | Email + Web | newPlan, newCredits, effectiveDate |
| Daily Reporting | ||
tenant_daily_report | tenantName, reportDate, periodSummary, activitiesDoneTable, activitiesDueTable, overdueTable, doneCount, dueCount, overdueCount | |
admin_tenant_report | reportDate, statsRow, newTenantsTable, onboardedTable, inactiveTable, newCount, onboardedCount, inactiveCount, grandTotal | |
system_usage_report | reportDate, statsRow, adapterBreakdownTable, totalRuns, totalCostUsd, totalInputTokens, totalOutputTokens, dailyActiveSessions, activeTenantCount | |
| Reporting | ||
monthly_report_ready | Email + Web | month, reportUrl, topMetric, topMetricValue |
| Ops (internal) | ||
agent_error | Web | � |
agent_health_check_failed | Web | � |
budget_cap_breached_usd | Web | � |
nightly_reconcile_drift | Web | � |
Related docs
- Design & implementation � file structure, handlers, templates, config, graceful shutdown
- Providers & resolution � per-tenant vs platform-default provider resolution
- Channels � per-channel handler details and provider packages