Skip to Content
ServersNotificationsservers/notifications � @leadmetrics/server-notifications

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

ConcernReason
IsolationProvider failures never affect the API or agent workers
Independent scalingHigh-volume email batches scale separately from agent workers
Provider swapsChanging from SendGrid to SES only touches this service
Rate limitingPer-queue concurrency limits enforce provider rate limits
Dev filteringDevelopment-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 hub

apps/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.

ChannelQueue nameConcurrencyAttemptsBackoff
Emailnotifications__email103exponential 5 s
WhatsAppnotifications__whatsapp33exponential 5 s
Telegramnotifications__telegram53exponential 5 s
SMSnotifications__sms23exponential 5 s
Web (in-app)notifications__web203exponential 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 : ? __ in dedupeKey (BullMQ v5 restriction)
  • jobId = sanitised dedupeKey, or auto-generated notif__<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

TypeChannelsKey variables
Onboarding
welcomeEmailfirstName, companyName, plan, dashboardUrl, currentYear
Workflow & HITL
approval_requiredEmail + WebactivityTitle, activityRef, reviewUrl
approval_expiring_soonEmail + WebactivityTitle, expiresAt, reviewUrl
approval_approvedWebactivityTitle, reviewerName
approval_rejectedEmail + WebactivityTitle, rejectionNote, reviewUrl
pipeline_preview_readyEmail + Webmonth, activityCount, estimatedCredits, reviewUrl
pipeline_blockedEmail + Web + SMSblockedTaskTitle, failureReason, pipelineUrl
activity_failedEmail + WebactivityTitle, activityRef, errorSummary
Credits & Billing
credits_warning_80Email + WebcreditsUsed, creditsTotal, creditsRemaining, resetDate
credits_exhaustedEmail + Web + SMSpausedActivities, topupUrl, resetDate
credits_topup_successEmail + WebcreditsAdded, newBalance, expiresAt
payment_failedEmail + SMSamountInr, retryDate, billingUrl
subscription_renewedEmailplan, amountInr, nextBillingDate
plan_upgradedEmail + WebnewPlan, newCredits, effectiveDate
Daily Reporting
tenant_daily_reportEmailtenantName, reportDate, periodSummary, activitiesDoneTable, activitiesDueTable, overdueTable, doneCount, dueCount, overdueCount
admin_tenant_reportEmailreportDate, statsRow, newTenantsTable, onboardedTable, inactiveTable, newCount, onboardedCount, inactiveCount, grandTotal
system_usage_reportEmailreportDate, statsRow, adapterBreakdownTable, totalRuns, totalCostUsd, totalInputTokens, totalOutputTokens, dailyActiveSessions, activeTenantCount
Reporting
monthly_report_readyEmail + Webmonth, reportUrl, topMetric, topMetricValue
Ops (internal)
agent_errorWeb
agent_health_check_failedWeb
budget_cap_breached_usdWeb
nightly_reconcile_driftWeb

© 2026 Leadmetrics — Internal use only