Devboxes
Feature Catalog

Transactional email

One template set for every outbound mail — multipart, canonical links, imprint footer.

Every mail the product sends — organization invitation, e-mail verification, e-mail change confirmation, password reset, two-factor code, feedback notification, and account/organization deletion confirmation — renders from one template set into a multipart message: a plain-text part that reads complete on its own with bare URLs, and a minimal HTML part carrying the same content. All links point at the canonical public app URL (BETTER_AUTH_URL); the request host is never used, because behind the ingress that host is internal. Who receives which mail follows from organization access: invitations are the only way into an organization, so these messages are the product's first impression — and its top spam-filter risk.

What every message carries

  1. Both parts, always — text and HTML (multipart/alternative).
  2. A subject that is never all-caps.
  3. Links only to the canonical app origin — no shorteners, no third-party hosts, no images (the logo is text).
  4. The imprint footer in both parts.
  5. The same sender on every mail, with an optional reply-to override per message (the feedback notification uses it so a reply reaches a person).
VariableRequiredCarries
MAIL_FROMyesThe From header on every mail, e.g. Devboxes <no-reply@devboxes.ai>
MAIL_FEEDBACK_TOyesThe inbox that receives feedback submitted from the dashboard
MAIL_IMPRINTyesThe legal name and postal address rendered into every footer (German imprint requirement)

MAIL_FROM replaced SMTP_FROM with no fallback. A deployment boots only with MAIL_FROM, MAIL_FEEDBACK_TO, and MAIL_IMPRINT set; production must carry the operator's real legal name and postal address in MAIL_IMPRINT.

How to test it

SuiteProves
pnpm vp run @firops/api#testtemplates.test.tsEvery template renders subject, text, and HTML from fixture data; the hygiene contract above holds for each one; interpolated names and messages cannot inject markup into the HTML part
pnpm vp run @firops/api#testinvitation-email.integration.test.tsInviting a member through the real auth flow sends exactly one message with the configured sender, the invitee as recipient, and a working accept URL in both parts
pnpm vp run @firops/api#test — data-rights integration suiteCompleted account and organization deletions send confirmation to the captured address after storage cleanup

Manual smoke: pnpm vp run @firops/api#email:preview renders every template with fixture data to HTML and text files and prints the output directory. The dev stack's compose Mailpit receives real sends for checking the wire shape.

Decisions

  • Plain template literals, no mail-template engine — a handful of templates does not earn an MJML/react-email dependency; the earlier react-email renderer was replaced so subject, text, and HTML render together from one readable module.
  • The text part is written by hand, not derived from the HTML — it must read complete on its own in a text-only client, with bare URLs a reader can copy.
  • Links derive from the canonical public app URL, never the request host — mail generated by an API pod behind the ingress must not leak an internal host into a link.
  • MAIL_FROM, MAIL_FEEDBACK_TO, and MAIL_IMPRINT are required with no compatibility alias — a deployment that cannot route complete mail fails at boot instead of sending from or to an implicit address.
  • Organization teams stay disabled — the teams switch was on while the database schema had no team tables, which made every invite fail with a server error; nothing used teams, so the switch went away rather than the schema growing. Re-enabling teams starts with the schema.
  • Deletion confirmations use the shared template at the completed boundary — account and organization deletion capture the recipient before erasure, complete storage cleanup, and then send the existing multipart confirmation.

Next: Repository connections — how code gets in.

Terms of ServicePrivacy Policy

On this page