Skip to main content

Notification System

Source: equa-server/modules/notifications/ Last Updated: 2026-02-28 Verified Against: Spec 021 verification-results.md (13/13 tasks PASS)

Overview

The notification system handles transactional email delivery for the Equa platform. It implements a Notifier interface that accepts a user ID and a notification object, renders HTML from Handlebars templates, and sends via a configurable transport layer. The module is backend-only with no dedicated API endpoints — it is consumed as a library by auth, admin, referral, billing, and organization modules.

Architecture

Core Interface

The Notifier type from common:
type Notifier = (user: Uuid | undefined, notification: Notification) => Promise<void>
The factory function newNodeMailerNotifier (email-notifier.ts:64-90) creates a Notifier that:
  1. Resolves recipient email via getContactInfo(user) or falls back to notification.args.to
  2. Looks up the template by notification.template name
  3. Renders HTML via the compiled Handlebars template
  4. Sends via transporter.sendMail({ from, to, bcc, subject, html })
  5. Calls optional onFinished callback
Throws if to is undefined or template is not found.

Email Transports

Transport is selected via the EMAIL_TRANSPORTER environment variable in services.ts:96-113:
EMAIL_TRANSPORTER ValueTransportFactory FunctionUse Case
(unset/empty)AWS SESnewNodemailerSesTransporterProduction email sending
smtpSMTPnewNodemailerSmtpTransporterStaging or alternative provider
devBuffernewNodemailerBufferTransporterLocal dev (saves .eml + .txt to temp/)
noneNo-OpemptyNotifierDisables all email sending

Transport Composition

combineNodeMailerMethods (email-notifier.ts:49-62) sends via multiple transports simultaneously using Promise.all. Returns the last transport’s response. Used in test utilities to combine dev + test methods.

Template System

Loading (templates.ts:27-35)

newEmailTemplateMap loads templates synchronously at startup:
  1. Reads all .handlebars files from src/partials/ and registers as Handlebars partials
  2. Reads all .handlebars files from src/templates/ and compiles with { noEscape: true }
  3. Returns a map of { templateName: compiledFunction }
noEscape: true means all Handlebars expressions render raw HTML without escaping. This is intentional for HTML email content but requires that template variables are sanitized upstream if they contain user-provided content.

Email Templates (10)

TemplatePartialConsumer ModuleCall Site
magicLinkmainauthmagic-link.ts:79
emailVerificationmainauthemail-verification.ts:43
passwordResetmainauthtemp-password.ts:82
passwordChangedmainauthupdate-user.ts:19
userInvitationmainNewreferralreferral.ts:428
sendCompanyInfomainreferralreferral.ts:521
organizationInvitationmainNeworganizations, adminorganizations/writing.ts:517, admin/writing.ts:42
organizationInvitationConvertiblemainNew(no active consumer)Defined but unused
contactSupportmainbillingbilling/writing.ts:237
adminOrganizationCreatedmainOrgorganizationsorganizations/writing.ts:408

Layout Partials (3)

PartialStructureTemplates Using It
mainOriginal Equa-branded layout: green (#33BB40/#33AA40) theme, centered 538px table, Equa logo from Azure blob, confidentiality footermagicLink, emailVerification, passwordReset, passwordChanged, sendCompanyInfo, contactSupport
mainNewUpdated layout: Nunito Sans font, dark header (#304651), 600px width, responsive mobile styles, dark/light mode supportuserInvitation, organizationInvitation, organizationInvitationConvertible
mainOrgOrganization-branded minimal layout: 100% width, Nunito Sans font, responsiveadminOrganizationCreated

AWS SES Configuration

newSesClient (aws.ts:6-13) creates an AWS SES client with API version 2010-12-01 using aws-sdk v2. newAwsConnectionConfig (aws.ts:15-21) builds credentials from environment:
VariableRequiredDefaultSource
AWS_ACCESS_KEY_IDYesaws.ts:17
AWS_SECRET_ACCESS_KEYYesaws.ts:18
AWS_SES_REGIONNous-east-1services.ts:110
The SES region is passed as a function parameter (not read from env directly by the notifications module). The wiring in services.ts reads AWS_SES_REGION and passes it through. These are separate credentials from S3 file storage (AWS_S3_ACCESS_KEY_ID / AWS_S3_SECRET_ACCESS_KEY).

Complete Environment Variables

VariableRequiredDefaultSource
EMAIL_TRANSPORTERNo(SES)services.ts:96-113
AWS_ACCESS_KEY_IDIf SESaws.ts:17
AWS_SECRET_ACCESS_KEYIf SESaws.ts:18
AWS_SES_REGIONNous-east-1services.ts:110
SMTP_HOSTIf SMTPmail.equastart.ioservices.ts:88
SMTP_PORTIf SMTP465services.ts:89
SMTP_SECUREIf SMTPtrueservices.ts:90
SMTP_USERIf SMTP''services.ts:91
SMTP_PASSIf SMTP''services.ts:92
FROM_EMAIL_NAMENoEquaservices.ts:140
FROM_EMAIL_ADDRESSNoequabot@equastart.ioservices.ts:141
GLOBAL_BCCNoservices.ts:116 (comma-separated)

Type Definitions

Core Types (types.ts)

TypeDefinition
EmailAddress{ name: string, address: string }
UserEmailGetter(id: string) => Promise<EmailAddress | undefined>
EmailTemplateArguments{ from: EmailAddress }
EmailTemplateMap{ [key: string]: (args: any) => string }
AwsConnectionConfig{ accessKeyId: string, secretAccessKey: string, region: string }

Transport Types (email-notifier.ts)

TypeDefinition
OnEmailFinished(response: any, user: Uuid | undefined, notification: Notification) => void
NodeMailerMethod{ transporter: any, onFinished?: OnEmailFinished }
SmtpConfig{ host: string, port: number, secure?: boolean, user?: string, pass?: string }

User Email Resolution

UserEmailGetter is implemented as getUserContactInfo in persistence/src/common/reading.ts:528-542:
  • SQL: SELECT "fullName", users."email" FROM "users" LEFT JOIN profiles ON profiles."id" = $1 WHERE "users"."id" = $1
  • Returns { name: fullName, address: email } or undefined if not found
  • Wired in services.ts:137 and passed to newNodeMailerNotifier

Legacy: Mandrill

The mandrill/ subdirectory contains a Mandrill (Mailchimp Transactional) implementation that is fully commented out. The index.ts re-exports types only. This is dead code from a previous email provider; current production uses Nodemailer + SES exclusively.

Known Issues

IssueSeverityDetails
Missing magicLink in authNotificationTemplatesHighmagic-link.ts:79 references authNotificationTemplates.magicLink which is undefined, causing runtime errors. Fix: add magicLink: 'magicLink' to auth/src/types.ts.
aws-sdk v2 deprecationMediumaws.ts:1 imports aws-sdk/clients/ses (v2, maintenance mode). Should migrate to @aws-sdk/client-ses v3.
No email queue/retryMediumsendMail() is called synchronously with no retry or dead letter queue. Relies on SES reliability.
noEscape: true in templatesMediumHTML is not escaped in template rendering. Low risk since variables come from server code, not user input.
Mandrill dead codeLowCommented-out implementation still exported from module index.