Skip to content

New: AI agent integration via Model Context Protocol (MCP).Learn more

Email & notifications

Evolve uses a two-layer system for transactional emails. Domain services publish notification events to a message queue through the @evolve-framework/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.

graph LR;
    svc["Domain service"] -- "publish notification" --> queue["Message queue"]
    queue --> email["email-smtp service"]
    email -- "render template" --> react["React Email"]
    email -- "send" --> smtp["SMTP server"]
    smtp --> inbox["Customer inbox"]

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.

Type Published by Variables
account-created Account service
order-confirmed Checkout service Order number, customer, billing/shipping address
order-shipped Checkout service Shipment details
password-reset-request Account service Reset token, expiry
password-reset-complete Account 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.

Domain services use QueuedUserNotificationClient to publish notifications:

const notificationClient = new QueuedUserNotificationClient();
notificationClient.configure(
config.COMPONENT_NAME,
resolvePublisher(config.INTERNAL_EVENTS_TARGET),
);
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.

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

  • Directoryemail-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.

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} />),
});

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

Variable Purpose
SMTP_HOST SMTP server hostname
SMTP_PORT SMTP server port
SMTP_SECURE Enable TLS
SMTP_USERNAME SMTP authentication username
SMTP_PASSWORD SMTP authentication password
EMAIL_FROM Default sender address
EMAIL_ALLOWLIST Comma-separated recipient filter

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.

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.

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