Notification Provider Packages
All concrete provider implementations live under packages/providers/. The
apps/servers/notifications server imports from these packages and never calls
provider APIs directly.
Package Inventory
packages/providers/
|-- sendgrid/ @leadmetrics/provider-sendgrid Live (unit + integration tests)
|-- smtp/ @leadmetrics/provider-smtp Live (unit tests)
|-- ses/ @leadmetrics/provider-ses Live (unit + integration tests)
|-- whatsapp/ @leadmetrics/provider-whatsapp Live (unit + integration tests)
|-- telegram/ @leadmetrics/provider-telegram Live (unit + integration tests)
|-- msg91/ @leadmetrics/provider-msg91 Planned
+-- twilio/ @leadmetrics/provider-twilio PlannedShared Interface � Email
All three email provider packages export the same EmailProvider interface. The
notifications server handler (email.handler.ts) depends only on this interface.
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; }
interface EmailProvider {
readonly name: string;
send(message: EmailMessage): Promise<EmailSendResult>;
}@leadmetrics/provider-sendgrid
| Field | Value |
|---|---|
| Class | SendGridProvider |
| Provider slug | sendgrid |
| External dep | @sendgrid/mail |
| Config type | SendGridConfig { apiKey, fromAddress, fromName? } |
| Tests | src/__tests__/sendgrid.test.ts (unit, vi.mock) |
| Integration tests | src/__tests__/sendgrid.integration.test.ts |
Usage:
import { SendGridProvider } from "@leadmetrics/provider-sendgrid";
const provider = new SendGridProvider({ apiKey: "SG.xxx", fromAddress: "noreply@acme.com" });
const result = await provider.send({ to: [{ name: "Alice", email: "alice@acme.com" }], subject: "Hi", html: "<p>Hi</p>" });@leadmetrics/provider-smtp
| Field | Value |
|---|---|
| Class | SmtpProvider |
| Provider slug | smtp |
| External dep | nodemailer |
| Config type | SmtpConfig { host, port, secure, user, pass, fromAddress, fromName? } |
| Tests | src/__tests__/smtp.test.ts (unit, vi.mock nodemailer) |
Supports any SMTP server � Gmail, Outlook 365, self-hosted Postfix, etc.
@leadmetrics/provider-ses
| Field | Value |
|---|---|
| Class | SesProvider |
| Provider slug | ses |
| External dep | @aws-sdk/client-ses + nodemailer |
| Config type | SesConfig { accessKeyId, secretAccessKey, region, fromAddress, fromName? } |
| Tests | src/__tests__/ses.test.ts (unit) + integration (skipped without AWS creds) |
Uses nodemailer’s SES transport so multipart MIME (text + HTML parts) works correctly.
@leadmetrics/provider-whatsapp
| Field | Value |
|---|---|
| Class | WhatsAppBusinessProvider |
| Provider slug | whatsapp_business_api |
| External dep | axios |
| Config type | WhatsAppBusinessConfig { apiKey, phoneNumberId, baseUrl? } |
| Tests | Unit + integration (skipped without Meta credentials) |
Calls Meta Graph API POST /<phoneNumberId>/messages.
Only pre-approved template messages are supported � free-form text is not allowed by Meta.
interface WhatsAppProvider {
readonly name: string;
sendTemplate(message: {
to: string; // E.164
template: { name: string; languageCode: string };
variables: Record<string, string>;
}): Promise<WhatsAppSendResult>;
}@leadmetrics/provider-telegram
| Field | Value |
|---|---|
| Class | TelegramBotProvider |
| Provider slug | telegram_bot |
| External dep | axios |
| Config type | TelegramBotConfig { botToken, defaultChatId?, baseUrl? } |
| Tests | Unit + integration (skipped without bot token) |
Calls Telegram Bot API POST /sendMessage.
interface TelegramProvider {
readonly name: string;
send(message: {
chatId: string | number;
text: string;
parseMode?: "HTML" | "Markdown" | "MarkdownV2";
disableWebPagePreview?: boolean;
}): Promise<TelegramSendResult>;
}Running Tests
# All provider unit tests
pnpm --filter "@leadmetrics/provider-*" test
# Single package
pnpm --filter @leadmetrics/provider-sendgrid test
# Integration tests (requires .env.test.local with real credentials)
pnpm --filter @leadmetrics/provider-sendgrid test:integration
pnpm --filter @leadmetrics/provider-telegram test:integrationIntegration tests are skipped (.skip) when the required credentials are absent.
Adding a New Provider Package
Use an existing package as a template (e.g. packages/providers/sendgrid/).
- Create
packages/providers/<name>/withpackage.json,tsconfig.json,vitest.config.ts,src/types.ts,src/index.ts,src/<name>.ts - Implement the appropriate provider interface (
EmailProvider,WhatsAppProvider, etc.) - Export the class and config type from
src/index.ts - Add unit tests using
vi.hoisted()for mock initialisation before imports - Add
case "<slug>":toresolver.tsinapps/servers/notifications - Update
.env.examplewith any new config fields