Appearance
Operations Alerts — Email Delivery
This document covers the Notifications module side of the operations email delivery pipeline. For the full end-to-end flow (scheduling, listener, hydration, rule lifecycle), see Email delivery on schedule arrival.
INotificationSender Port
INotificationSender is the domain-agnostic port that operations (and any future caller) uses to send emails without knowing the underlying SMTP implementation.
Path: apps/api/src/modules/notifications/domain/ports/notification-sender.port.tsDI token: NotificationSenderKey = Symbol('INotificationSender')
ts
export interface ISendEmailInput {
templateKey: string;
recipients: Array<{ email: string; name: string }>;
variables: Record<string, unknown>;
}
export interface INotificationSender {
sendEmail(input: ISendEmailInput): Promise<void>;
}Contract semantics
- Throws on failure — any SMTP error or partial delivery failure causes
sendEmailto throw. The caller (operations listener) lets the exception propagate so BullMQ retries the job. - Partial delivery semantics — the adapter iterates recipients in order. Each recipient's email-send attempt is independent: a failed attempt saves a
NotificationLogrow withsuccess = falseand increments a failure counter. After the loop, iffailedCount > 0, the method throws with a summary message. This ensures allNotificationLogrows are written before the exception propagates. - Template-not-found — if the
templateKeyis not registered, throws immediately before any SMTP call. Callers should treat this as a programming error (wrong key), not a transient failure.
This is a deliberate departure from SendEmailNotificationCommand.execute, which returns Result<T, Error> and never throws. The port throws loud because the listener needs error propagation for BullMQ retry semantics.
Template Registry Entries
The four operations email templates are registered in TemplateRegistryAdapter under the following keys (defined in the EmailTemplates const):
| Key | Const name | Template component |
|---|---|---|
operations.event.start | EmailTemplates.OperationsEventStart | OperationsEventStartTemplate |
operations.event.end | EmailTemplates.OperationsEventEnd | OperationsEventEndTemplate |
operations.task.start | EmailTemplates.OperationsTaskStart | OperationsTaskStartTemplate |
operations.task.end | EmailTemplates.OperationsTaskEnd | OperationsTaskEndTemplate |
Template prop shape
All four templates share the same props interface:
ts
interface OperationsEmailTemplateProps {
recipientName: string; // resolved by adapter from recipient.name
subjectTitle: string; // event.title OR task description (truncated to 80 chars)
whenLabel: string; // pre-formatted Spanish date/time string, e.g. "21 de abril a las 14:30"
}whenLabel is a pre-formatted string produced by OperationsSubjectHydrator using Intl.DateTimeFormat('es', { dateStyle: 'long', timeStyle: 'short' }). Templates never receive raw Date objects — formatting is the hydrator's responsibility.
recipientName is merged into variables by the adapter at send time: { ...variables, recipientName: recipient.name }.
Subject lines (Spanish)
| Template key | Subject |
|---|---|
operations.event.start | Tu evento "{subjectTitle}" está por comenzar |
operations.event.end | Tu evento "{subjectTitle}" está por finalizar |
operations.task.start | Tu tarea "{subjectTitle}" está por iniciar |
operations.task.end | Tu tarea "{subjectTitle}" está por cerrarse |
Open question: Spanish copy is pending stakeholder review. The subjects and body text above reflect the initial implementation and may change before GA.
SMTP Dependency and NodemailerNotificationSenderAdapter
Path: apps/api/src/modules/notifications/infrastructure/adapters/nodemailer-notification-sender.adapter.ts
The adapter implements INotificationSender and is registered under NotificationSenderKey in NotificationsInfrastructureAdapters. NotificationsModule exports NotificationSenderKey so that OperationsModule (which imports NotificationsModule) can inject it.
Dependencies injected into the adapter:
| Port | Token | Purpose |
|---|---|---|
ITemplateRegistryPort | TemplateRegistryPortKey | Template lookup by key |
IEmailPort | EmailAdapterKey | Nodemailer SMTP send |
INotificationLogRepository | NotificationLogRepositoryKey | Persist NotificationLog per recipient |
IAppLoggerService | AppLoggerServiceKey | Structured logging |
Adapter behavior per recipient
- Look up template — throw
templateNotFoundif missing. - Merge
variableswith{ recipientName: recipient.name }. - Call
template.subject(mergedVars)to get the email subject. - Call
template.render(mergedVars)to render HTML. - Call
emailPort.send({ to, subject, html }). - Save
NotificationLogwithsuccess = true / false. - Log
notifications.sender.sent(success) ornotifications.sender.failed(SMTP error). - If SMTP threw: increment
failedCount. - After the loop: if
failedCount > 0— throw with a summary message.
Domain Boundary Note
The Notifications module stays domain-agnostic. It knows nothing about:
NotificationRuleentities or their lifecycle.OperationsSubjectHydratoror how recipients are resolved.- The
schedule.arrivedevent or BullMQ retry semantics.
All operations-specific logic (rule validation, idempotency guard, hydration, recipient deduplication) lives in the Operations module's listener and hydrator. The Notifications module only provides:
- The
INotificationSenderport (throw-on-failure contract). - The SMTP adapter implementation.
- The template registry with operations template entries.
This keeps the Notifications module reusable for other future callers (e.g., a different module triggering alerts).
Cross-Links
- Email delivery on schedule arrival — full end-to-end flow from
schedule.arrivedto SMTP. - Email Sending Workflow — general email sending via
SendEmailNotificationCommand. - Notifications Module Overview