Overview
Email is provider-agnostic behind src/lib/email/. It supports Resend (the
default) and SMTP via Nodemailer, chosen in src/config/email.config.ts.
Templates are React Email components in src/emails/.
EMAIL_PROVIDER=resend # "resend" | "nodemailer"
EMAIL_FROM=[email protected]
EMAIL_FROM_NAME="Your App"
EMAIL_REPLY_TO=[email protected]
RESEND_API_KEY=re_... # when provider=resend
# or SMTP_HOST / SMTP_PORT / SMTP_SECURE / SMTP_USER / SMTP_PASS
Sending mail
Prefer the named senders in src/lib/email/index.ts over calling the
low-level sendEmail() directly — they render the right template with typed
props:
await sendPasswordResetEmail({ to, url, locale })
await sendOrgInvitationEmail({ to, orgName, inviterEmail, url, locale })
await sendMagicLinkEmail({ to, url, locale })
The kit ships senders for password reset, email verification, magic link, org invitation (and revocation), email-change verification, contact form, and the billing lifecycle (payment failed, subscription canceled, dispute created, trial ending).
The generic sender remains available for one-off mail:
await sendEmail({ to, subject, html, text })
Templates are localized
Every template accepts a locale prop and renders translated strings, so an
invitation reads in the recipient's language. Templates share a common
EmailLayout. To preview them while editing:
pnpm email:dev
Adding a template
- Create a React Email component in
src/emails/that takes typed props (includinglocale). - Add a named sender in
src/lib/email/index.tsthat renders it and callssendEmail(). - Call your sender from the relevant tRPC procedure or route.
Failures don't crash callers
sendEmail() catches and logs provider errors (redacting the recipient) rather
than throwing, so a transient email outage doesn't take down the request that
triggered it. Delivery problems surface in your logs and Sentry, not as user
errors.