Guide

Mails

packages/mail owns transactional email: Maizzle-based templates plus a small provider layer. The dashboard uses it for organization invitations and other auth-driven messages.

packages/mail owns transactional email: Maizzle-based templates plus a small provider layer. The dashboard uses it for organization invitations and other auth-driven messages.

Providers

MAIL_PROVIDER selects the transport. When it is absent, a configured RESEND_API_KEY selects Resend so hosted integrations such as Vercel's work without a second selector variable. With neither set, the default is console, which logs instead of delivering — an unconfigured deployment cannot silently attempt real delivery. An explicit MAIL_PROVIDER always wins.

ProviderConfiguration
consoleNone. Logs the rendered message. The credential-free default; not for production.
resendRESEND_API_KEY
smtpSMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_SECURE

MAIL_FROM sets the sender address for every provider.

SMTP_SECURE selects implicit TLS: true on port 465, false on port 587 (STARTTLS).

Templates

Templates are authored with Maizzle and declared in the package's template registry:

  • mailTemplates maps a MailTemplateId to its template file, subject, and context fields;
  • MailTemplateContext types the data each template needs;
  • maizzle.config.ts is discovered by Maizzle's own render() call by filesystem convention.

Adding a template means adding it to the registry with a typed context, keeping delivery and content concerns separate: providers never know what a template says, and templates never know how mail is sent.

Compiling templates

Rendering a template runs Maizzle's pipeline — Vue SSR, CSS inlining, plaintext — on top of a Vite SSR server, which needs a bundler with its platform-native binary and a writable working directory. The deployments that send mail are serverless functions with neither, so rendering happens once at build time instead:

aube run mail:compile

That writes packages/mail/src/util/compiled-templates.json, which is checked in and bundled into the package. Each template is rendered once per combination of the conditional fields the registry declares — the halves a template renders away when empty — with every other field standing in as a placeholder. sendEmail then picks the variant its context matches and substitutes the values, escaping them for the HTML part and leaving them bare in the plaintext one.

Run it after editing anything under packages/mail/emails/. aube test fails when the checked-in artifact no longer matches what the templates render.

Previewing templates

apps/mail-preview wraps Maizzle's dev server around the package's templates:

aube run mail:preview

This serves every template at http://localhost:3005 with hot reload, rendered against the sample values in apps/mail-preview/maizzle.config.ts. The app owns no templates of its own — its emails/ directory is a symlink into packages/mail/emails/, so edits there reload live. Its build script compiles every template to static HTML in apps/mail-preview/dist/, which CI runs as a template-compilation check.

Organizations depend on mailOrganization invitations are delivered by email, so enabling organizations requires a real transport (resend or smtp).
Copyright © 2026