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
| 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.
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:
| Variable | Purpose |
|---|---|
EMAIL_SMTP_HOST | SMTP server hostname |
EMAIL_SMTP_PORT | SMTP server port |
EMAIL_SMTP_SECURE | Enable TLS |
EMAIL_SMTP_USERNAME | SMTP authentication username |
EMAIL_SMTP_PASSWORD | SMTP authentication password |
EMAIL_FROM | Default sender address |
EMAIL_ALLOWLIST | Comma-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:
- Define the schema: add a new JSON Schema in
schemas/src/notifications/with the notification's variables - Generate types: run
pnpm codegento generate the Zod schema and TypeScript types - Add the client method: add a typed method to
QueuedUserNotificationClientin theuser-notificationspackage - Publish from the domain service: call the new method where the business event occurs
- Create the email template: add a React Email component in
email-smtp/src/emails/ - Add translations: add translation keys for all supported locales
- Add the handler: register a case in the email service's
processNotificationEventswitch statement - Subscribe the queue: if using a new event type, add an EventBridge rule (AWS) or equivalent subscription for the new type