Skip to Content
ServersNotificationsNotification Providers & Resolution

Notification Providers & Resolution

Every channel supports two modes: tenant-configured (bring your own provider) and platform default (works out of the box). The resolver in src/resolver.ts picks at dispatch time � tenants never need to configure a provider for notifications to work.


Resolution Pattern

Job arrives for tenantId X, channel = email | v resolveEmailProvider("X") | |-- db.notificationProvider.findFirst({ | where: { | tenantId: "X", | channel: "email", | verifiedAt: { not: null }, // unverified rows are skipped | } | }) | |-- Row found (provider = "smtp") -> new SmtpProvider(decrypt(row.config)) |-- Row found (provider = "sendgrid") -> new SendGridProvider(decrypt(row.config)) |-- Row found (provider = "ses") -> new SesProvider(decrypt(row.config)) +-- No row / not verified -> new SendGridProvider(platform env vars)

Same pattern for WhatsApp and Telegram. Web notifications always use the platform (there is no per-tenant web push provider).

Encryption note: decrypt() is currently a JSON.parse passthrough during development. In production it must be replaced with AES-256-GCM decryption from @leadmetrics/crypto before provider configs are stored in the DB.


Why Tenants Configure Their Own Providers

BenefitDetail
Email deliverabilityEmail from noreply@yourbrand.com (SPF + DKIM on their domain) lands better than a shared platform sender
WhatsApp brand presenceMessages come from the tenant’s verified WhatsApp Business number
Existing contractsMany agencies already have SendGrid / Twilio accounts with negotiated rates
ComplianceSome enterprise clients require outbound comms from their own infrastructure

Database Table � notification_provider

One row per channel per tenant. Only a verified row is used � unverified rows fall back to the platform default.

model NotificationProvider { id String @id @default(cuid()) tenantId String channel String // "email" | "whatsapp" | "telegram" provider String // "smtp" | "sendgrid" | "ses" | "whatsapp_business_api" | "telegram_bot" config String // JSON (will be AES-256-GCM encrypted in production) verifiedAt DateTime? // NULL = configured but not tested; unverified = platform default used lastError String? // shown in Dashboard settings on test failure createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenant Tenant @relation(...) @@unique([tenantId, channel]) // one active provider per channel per tenant @@map("notification_provider") }

Provider Packages

All concrete providers are in packages/providers/. Each package owns one provider type.

@leadmetrics/provider-sendgrid � Email

interface EmailProvider { readonly name: string; send(message: EmailMessage): Promise<EmailSendResult>; } interface EmailMessage { to: { name: string; email: string }[]; subject: string; html: string; text?: string; replyTo?: { name: string; email: string }; attachments?: { filename: string; content: Buffer; contentType: string }[]; } interface EmailSendResult { messageId: string; provider: string; }

Class: SendGridProvider
Config: { apiKey: string; fromAddress: string; fromName?: string }
Dep: @sendgrid/mail

@leadmetrics/provider-smtp � Email (SMTP / Gmail / Outlook / Postfix)

Class: SmtpProvider
Config: { host: string; port: number; secure: boolean; user: string; pass: string; fromAddress: string; fromName?: string }
Dep: nodemailer

@leadmetrics/provider-ses � Email (AWS SES)

Class: SesProvider
Config: { accessKeyId: string; secretAccessKey: string; region: string; fromAddress: string; fromName?: string }
Dep: @aws-sdk/client-ses + nodemailer SES transport

@leadmetrics/provider-whatsapp � WhatsApp Business API

interface WhatsAppProvider { readonly name: string; sendTemplate(message: WhatsAppMessage): Promise<WhatsAppSendResult>; } interface WhatsAppMessage { to: string; // E.164 template: { name: string; languageCode: string }; variables: Record<string, string>; }

Class: WhatsAppBusinessProvider
Config: { apiKey: string; phoneNumberId: string; baseUrl?: string }
Dep: axios ? Meta Graph API v19.0
Note: WhatsApp only supports pre-approved Meta templates. See channel doc for the registered template map.

@leadmetrics/provider-telegram � Telegram Bot

interface TelegramProvider { readonly name: string; send(message: TelegramMessage): Promise<TelegramSendResult>; } interface TelegramMessage { chatId: string | number; text: string; parseMode?: "HTML" | "Markdown" | "MarkdownV2"; disableWebPagePreview?: boolean; }

Class: TelegramBotProvider
Config: { botToken: string; defaultChatId?: string; baseUrl?: string }
Dep: axios ? Telegram Bot API

SMS � Planned

PackageClassProviderStatus
@leadmetrics/provider-msg91Msg91ProviderMSG91 (India)Planned
@leadmetrics/provider-twilioTwilioProviderTwilio (International)Planned

Provider Config Per Channel

Configs stored in notification_provider.config (JSON, encrypted in production):

Email

// smtp { "host": "smtp.gmail.com", "port": 587, "secure": false, "user": "bot@acme.com", "pass": "...", "fromAddress": "noreply@acme.com" } // sendgrid { "apiKey": "SG.xxx", "fromAddress": "noreply@acme.com", "fromName": "Acme" } // ses { "accessKeyId": "AKIA...", "secretAccessKey": "...", "region": "ap-south-1", "fromAddress": "noreply@acme.com" }

WhatsApp

{ "apiKey": "EAA...", "phoneNumberId": "123456789", "baseUrl": "https://graph.facebook.com/v19.0" }

Telegram

{ "botToken": "123456:ABC-...", "defaultChatId": "-100..." }

Platform Defaults (Environment Variables)

If no tenant-specific row exists (or it is unverified), these platform-level env vars are used. Tenants do not need to configure anything for notifications to work.

ChannelEnv vars
Email (SendGrid)SENDGRID_API_KEY, EMAIL_FROM_ADDRESS, EMAIL_FROM_NAME
WhatsAppWHATSAPP_API_KEY, WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_BASE_URL
TelegramTELEGRAM_BOT_TOKEN, TELEGRAM_DEFAULT_CHAT_ID

Adding a New Email Provider

  1. Create packages/providers/<name>/ � same structure as provider-sendgrid
  2. Implement the EmailProvider interface (same send() signature)
  3. Add case "<name>": to resolveEmailProvider() in apps/servers/notifications/src/resolver.ts
  4. Add unit tests with vi.hoisted() mock pattern (see provider-sendgrid tests)

© 2026 Leadmetrics — Internal use only