SsuperslateDocs

Transactional email

Configure Resend, preview email templates, and keep local development zero-key.

Transactional email

React Email source templates, deterministic rendered HTML, a typed server renderer, and Resend form one transactional-email path. Authentication can be developed locally without a provider account; production cannot.

Modes

ModeRESEND_API_KEYBehavior
Local/testunsetno provider call; recipient, template, subject, and props/action URL are logged
Provider testset to test keyrendered HTML sent through Resend; provider errors fail the calling operation
Productionrequiredstartup environment validation fails when absent

The local fallback is intentional for a single trusted developer machine. It is not a fake inbox: auth URLs appear in structured logs and are sensitive bearer links.

Source and generated artifacts

The editable source lives in packages/email/src. The deploy command exports, verifies, and copies HTML into apps/server/email-templates:

vp run --filter @app/email dev
vp run --filter @app/email deploy

Never edit apps/server/email-templates/*.html manually. The server requires and preloads all nine files before accepting traffic:

  • login-email;
  • verify-email;
  • reset-password;
  • welcome-email;
  • billing-subscription-created;
  • billing-payment-successful;
  • billing-payment-failed;
  • billing-subscription-canceled;
  • security-alert.

The source inventory, deploy manifest, typed renderer names/props/subjects, and generated HTML must remain exactly aligned. Missing templates fail deployment or startup instead of failing only when a buyer requests a password reset.

Rendering and delivery

Callers select a typed template and provide its exact props. The server:

  1. retrieves the preloaded HTML;
  2. applies conditional blocks;
  3. HTML-escapes every inserted value;
  4. resolves the static or typed subject;
  5. logs locally or sends from, recipients, subject, HTML, and optional reply-to through Resend.

Set:

RESEND_API_KEY=<API runtime secret>
EMAIL_FROM=Product <noreply@mail.example.com>

EMAIL_FROM must use a sender/domain verified in the same Resend account as the key. Keep the key only in the API environment; never expose it through VITE_*.

The compiled server still reads external rendered templates. The included Docker image copies them to its detected layout. Use EMAIL_TEMPLATES_DIR only for a proven custom filesystem layout; an explicit missing directory is a fatal startup error.

Delivery semantics

Provider rejection is returned as a stable server error to an awaited calling flow. Better Auth email calls are awaited, so signup/reset/magic-link failure remains visible to that auth operation.

Billing lifecycle notifications run after the financial transaction commits. A delivery failure is logged and cannot roll back billing state or force a webhook retry.

The v1 mailer does not include:

  • a durable outbox;
  • scheduled retries or dead letters;
  • delivery/bounce/complaint webhook processing;
  • application-owned send receipts;
  • provider idempotency keys.

Resend supports 24-hour idempotency keys, but adding a header alone would not fix a process crash between database commit and send. Add a product-owned outbox/state transition when an email is a business-critical deliverable rather than claiming exactly-once delivery.

References: Resend send API, idempotency keys, and errors.

Security and privacy

  • Production must never use the log fallback.
  • Local logs can contain verification, reset, and magic-link URLs; do not forward them to Sentry, analytics, shared chat, or public CI.
  • Sentry console breadcrumbs are disabled for this reason.
  • Do not log provider keys, complete provider responses, cookies, or unrelated template data.
  • Keep email props minimal; transactional email is not an analytics warehouse.
  • Product-specific unsubscribe, retention, consent, and marketing rules are buyer responsibilities.
  • A rendered link must use the exact trusted application origin and HTTPS in production.

Verification

For every template change:

vp run --filter @app/email deploy
bun test --cwd apps/server src/infra/email/renderer.test.ts src/infra/mailer/client.test.ts
vp run --filter @app/server build
git diff --check

Then send through a verified test domain and inspect subject, sender, links, mobile layout, dark mode, spam result, and provider error/log behavior. A source preview alone is not production proof.

Consult Add a transactional email and Deploy a fresh application.

Failure guide

FailureInspect
Local message “missing”structured server log and correct local fallback mode
Resend sender rejectedverified domain, EMAIL_FROM, key/account/environment
Link points to localhost/wrong hostFRONTEND_URL, SERVER_URL, Better Auth trusted origins
Placeholder remains in messagesource/deploy/renderer inventory and the deploy command above
Server exits before readyrendered directory, all nine files, EMAIL_TEMPLATES_DIR
Billing state correct but email absentpost-commit notification log; do not replay the financial event
Duplicate critical messageno v1 outbox/idempotency contract; design domain-owned delivery state

On this page