> ## Documentation Index
> Fetch the complete documentation index at: https://docs.equa.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# Notification System

> Email notifications via AWS SES and SMTP with Handlebars templates

# 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

```mermaid theme={null}
graph LR
    subgraph consumers [Consumer Modules]
        Auth["Auth\n(magic link, verification,\npassword reset/changed)"]
        Referral["Referral\n(user invitation,\ncompany info)"]
        Organizations["Organizations\n(org invitation,\nadmin org created)"]
        Admin["Admin\n(org invitation)"]
        Billing["Billing\n(contact support)"]
    end

    subgraph notifications [Notification Module]
        Notifier["Notifier Interface\n(user, notification) => void"]
        TemplateMap["Template Map\n10 Handlebars templates"]
        Partials["Layout Partials\nmain, mainNew, mainOrg"]
        UserLookup["UserEmailGetter\nuuid => EmailAddress"]
    end

    subgraph transport [Transport Layer]
        SES["AWS SES\n(production)"]
        SMTP["SMTP Server\n(staging/alt)"]
        Buffer["Buffer\n(dev/test)"]
        NoOp["No-Op\n(disabled)"]
    end

    Auth --> Notifier
    Referral --> Notifier
    Organizations --> Notifier
    Admin --> Notifier
    Billing --> Notifier
    Notifier --> UserLookup
    Notifier --> TemplateMap
    TemplateMap --> Partials
    Notifier --> SES
    Notifier --> SMTP
    Notifier --> Buffer
    Notifier --> NoOp
```

## Core Interface

The `Notifier` type from `common`:

```typescript theme={null}
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` Value | Transport | Factory Function                 | Use Case                                     |
| ------------------------- | --------- | -------------------------------- | -------------------------------------------- |
| (unset/empty)             | AWS SES   | `newNodemailerSesTransporter`    | Production email sending                     |
| `smtp`                    | SMTP      | `newNodemailerSmtpTransporter`   | Staging or alternative provider              |
| `dev`                     | Buffer    | `newNodemailerBufferTransporter` | Local dev (saves `.eml` + `.txt` to `temp/`) |
| `none`                    | No-Op     | `emptyNotifier`                  | Disables 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 }`

<Note>
  `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.
</Note>

### Email Templates (10)

| Template                            | Partial   | Consumer Module      | Call Site                                             |
| ----------------------------------- | --------- | -------------------- | ----------------------------------------------------- |
| `magicLink`                         | `main`    | auth                 | `magic-link.ts:79`                                    |
| `emailVerification`                 | `main`    | auth                 | `email-verification.ts:43`                            |
| `passwordReset`                     | `main`    | auth                 | `temp-password.ts:82`                                 |
| `passwordChanged`                   | `main`    | auth                 | `update-user.ts:19`                                   |
| `userInvitation`                    | `mainNew` | referral             | `referral.ts:428`                                     |
| `sendCompanyInfo`                   | `main`    | referral             | `referral.ts:521`                                     |
| `organizationInvitation`            | `mainNew` | organizations, admin | `organizations/writing.ts:517`, `admin/writing.ts:42` |
| `organizationInvitationConvertible` | `mainNew` | (no active consumer) | Defined but unused                                    |
| `contactSupport`                    | `main`    | billing              | `billing/writing.ts:237`                              |
| `adminOrganizationCreated`          | `mainOrg` | organizations        | `organizations/writing.ts:408`                        |

### Layout Partials (3)

| Partial   | Structure                                                                                                                            | Templates Using It                                                                                        |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `main`    | Original Equa-branded layout: green (#33BB40/#33AA40) theme, centered 538px table, Equa logo from Azure blob, confidentiality footer | `magicLink`, `emailVerification`, `passwordReset`, `passwordChanged`, `sendCompanyInfo`, `contactSupport` |
| `mainNew` | Updated layout: Nunito Sans font, dark header (#304651), 600px width, responsive mobile styles, dark/light mode support              | `userInvitation`, `organizationInvitation`, `organizationInvitationConvertible`                           |
| `mainOrg` | Organization-branded minimal layout: 100% width, Nunito Sans font, responsive                                                        | `adminOrganizationCreated`                                                                                |

## 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:

| Variable                | Required | Default     | Source            |
| ----------------------- | -------- | ----------- | ----------------- |
| `AWS_ACCESS_KEY_ID`     | Yes      | --          | `aws.ts:17`       |
| `AWS_SECRET_ACCESS_KEY` | Yes      | --          | `aws.ts:18`       |
| `AWS_SES_REGION`        | No       | `us-east-1` | `services.ts:110` |

<Note>
  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`).
</Note>

## Complete Environment Variables

| Variable                | Required | Default                | Source                              |
| ----------------------- | -------- | ---------------------- | ----------------------------------- |
| `EMAIL_TRANSPORTER`     | No       | (SES)                  | `services.ts:96-113`                |
| `AWS_ACCESS_KEY_ID`     | If SES   | --                     | `aws.ts:17`                         |
| `AWS_SECRET_ACCESS_KEY` | If SES   | --                     | `aws.ts:18`                         |
| `AWS_SES_REGION`        | No       | `us-east-1`            | `services.ts:110`                   |
| `SMTP_HOST`             | If SMTP  | `mail.equastart.io`    | `services.ts:88`                    |
| `SMTP_PORT`             | If SMTP  | `465`                  | `services.ts:89`                    |
| `SMTP_SECURE`           | If SMTP  | `true`                 | `services.ts:90`                    |
| `SMTP_USER`             | If SMTP  | `''`                   | `services.ts:91`                    |
| `SMTP_PASS`             | If SMTP  | `''`                   | `services.ts:92`                    |
| `FROM_EMAIL_NAME`       | No       | `Equa`                 | `services.ts:140`                   |
| `FROM_EMAIL_ADDRESS`    | No       | `equabot@equastart.io` | `services.ts:141`                   |
| `GLOBAL_BCC`            | No       | --                     | `services.ts:116` (comma-separated) |

## Type Definitions

### Core Types (`types.ts`)

| Type                     | Definition                                                         |
| ------------------------ | ------------------------------------------------------------------ |
| `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`)

| Type               | Definition                                                                       |
| ------------------ | -------------------------------------------------------------------------------- |
| `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

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

## Related

* [Notifications Guide](/guides/notifications) -- End-user guide
* [Auth and Permissions](/architecture/auth-and-permissions) -- Authentication triggers
* [File Storage Architecture](/architecture/file-storage-architecture) -- Shares AWS credential patterns
* [Environments and Config](/architecture/environments-and-config) -- Environment variable reference
