Skip to main content

Email & notifications

Evolve uses a two-layer system for transactional emails. Domain services publish notification events to a message queue through the @evolve-packages/user-notifications package. The email-smtp service consumes these events, renders emails using React Email templates, and delivers them via SMTP.

This separation means domain services never deal with email rendering or delivery. They just say "notify the customer about X" and the email service handles the rest.

Architecture

The notification client validates each event against a Zod schema before publishing, ensuring the email service always receives well-formed data. The message queue provides reliable delivery with retries and dead-letter handling.

Notification types

TypePublished byVariables
account-createdAccount service
order-confirmedCheckout serviceOrder number, customer, billing/shipping address
order-shippedCheckout serviceShipment details
password-reset-requestAccount serviceReset token, expiry
password-reset-completeAccount service

All notifications carry a shared envelope:

{
type: "order-confirmed",
origin: "checkout-commercetools",
recipients: [{ email: "customer@example.com", name: "Jane Doe" }],
storeContext: { storeKey: "main-store", locale: "en-GB" },
timestamp: "2025-01-15T10:30:00Z",
variables: { orderNumber: "ORD-2025-001", /* ... */ }
}

The messageGroupId is an MD5 hash of the sorted recipient email list, which ensures FIFO ordering per recipient where the transport supports it.

Publishing notifications

Domain services use QueuedUserNotificationClient to publish notifications:

const notificationClient = new QueuedUserNotificationClient(
publishMessage(config.NOTIFICATION_QUEUE)
);

await notificationClient.sendOrderConfirmation({
recipients: [{ email: customer.email, name: customer.name }],
storeContext: { storeKey, locale },
variables: { orderNumber, customer, billingAddress, shippingAddress },
});

Each notification type has a dedicated method that validates the payload at compile time and runtime.

Email rendering

The email-smtp service renders emails using React Email JSX templates. Each notification type has a corresponding template:

email-smtp/src/emails/
├── account-created.tsx
├── order-confirmation.tsx
├── order-shipped.tsx
├── password-reset-complete.tsx
└── password-reset-request.tsx

The render function produces both HTML and plain-text versions in parallel using @react-email/render. The plain-text version automatically strips images and preview elements.

Internationalization

Templates use a custom useTranslations(scope, locale) hook backed by intl-messageformat. Translations are stored as JSON files per locale and support ICU message format for plurals and selects:

Supported locales: de-DE, en-GB, es-ES, fr-BE, fr-FR, it-IT, nl-BE, nl-NL

Each email handler loads translations for the notification's locale and passes the t() function to the template:

const t = useTranslations("OrderConfirmation", locale);

await sendMail({
recipients,
subject: t("subject", { orderNumber }),
...render(<OrderConfirmation locale={locale} {...variables} />),
});

SMTP transport

The email service connects to any SMTP-compatible server. Configuration is through environment variables:

VariablePurpose
EMAIL_SMTP_HOSTSMTP server hostname
EMAIL_SMTP_PORTSMTP server port
EMAIL_SMTP_SECUREEnable TLS
EMAIL_SMTP_USERNAMESMTP authentication username
EMAIL_SMTP_PASSWORDSMTP authentication password
EMAIL_FROMDefault sender address
EMAIL_ALLOWLISTComma-separated recipient filter

Allowlist filtering

In non-production environments, the EMAIL_ALLOWLIST variable restricts delivery to matching recipients only. The filter uses domain suffix matching, so @labdigital.nl allows all addresses at that domain. When the allowlist is unset (production), all recipients are allowed.

This prevents accidental emails to real customers from staging or development environments.

Local development

For local development, Mailpit runs as a Docker container (started by task docker:services) and captures all outgoing email on localhost:1025. The Mailpit web UI at localhost:8025 lets you inspect rendered emails without any SMTP configuration.

Adding a new notification

To add a new notification type:

  1. Define the schema: add a new JSON Schema in schemas/src/notifications/ with the notification's variables
  2. Generate types: run pnpm codegen to generate the Zod schema and TypeScript types
  3. Add the client method: add a typed method to QueuedUserNotificationClient in the user-notifications package
  4. Publish from the domain service: call the new method where the business event occurs
  5. Create the email template: add a React Email component in email-smtp/src/emails/
  6. Add translations: add translation keys for all supported locales
  7. Add the handler: register a case in the email service's processNotificationEvent switch statement
  8. Subscribe the queue: if using a new event type, add an EventBridge rule (AWS) or equivalent subscription for the new type