Transactional email with React Email, Resend, and local Mailpit helpers.
Transactional email lives in packages/mail. Templates are React components styled with Tailwind CSS via React Email, rendered to HTML, then sent with sendEmail.
Why React Email
Style emails with Tailwind like the rest of the app, compose shared pieces
(for example Footer), and preview templates in the React Email dev server.
Resend
Production path — what sendEmail uses today.
Mailpit
Local SMTP capture via Docker (helper exists; routing commented out).
Sending a mail
Import sendEmail from @repo/mail and pass a rendered template as react:
import { sendEmail, MagicLinkSignIn } from "@repo/mail";
await sendEmail({
email: [user.email],
subject: "Sign in to TurboJet",
react: MagicLinkSignIn({
email: user.email,
url,
locale: "en",
copy: messages.magicLink,
}),
});SendEmailOptions (packages/mail/src/send-email.ts):
| Option | Type | Notes |
|---|---|---|
email | string[] | Recipients |
subject | string | |
react | ReactElement | An already-rendered template component |
from | string (optional) | Defaults to RESEND_EMAIL_DOMAIN |
There is no template name registry — you import components and pass the element yourself.
Templates
Templates live in packages/mail/src/emails:
MagicLinkSignIn.tsx— magic-link sign-in/sign-upEmailOtpSignIn.tsx— one-time codecomponents/Footer.tsx— shared footer; interpolates{email}into themessagecopy string
Each template composes @react-email/components (Html, Body, Container, Tailwind, …) directly. Props share the same shape: mail data, optional locale, and a copy object of already-resolved strings.
Create a new template
- Add a
.tsxfile underpackages/mail/src/emails(mirror an existing template’scopyprop). - Export it from
packages/mail/src/index.ts. - Optionally add a subpath in
packages/mail/package.jsonexports. - Add copy keys in
packages/i18n/src/mail/en.jsonandid.json. - Call
sendEmailwith the component asreact, passinglocaleand resolvedcopy.
See the React Email docs for available components.
Translations
Mail copy lives in packages/i18n/src/mail/en.json and id.json (magicLink, emailOtp). Templates do not call createTranslator — callers load strings with getMailMessages(locale) from @repo/i18n and pass them as copy.
import { getMailMessages } from "@repo/i18n";
const messages = await getMailMessages(locale);
// messages.magicLink → MagicLinkSignIn `copy`getMailMessages falls back to the default locale for anything other than en / id.
Where it's used
packages/auth/src/index.ts sends both mails from Better Auth plugins:
magicLink.sendMagicLink→MagicLinkSignInemailOTP.sendVerificationOTP(type"sign-in"only) →EmailOtpSignIn
Locale is resolved in resolveMailLocale(): locale cookie, then Accept-Language, then the default locale. See Authentication.
Previewing emails
There is no separate apps/mail-preview app. Use the package script:
pnpm --filter @repo/mail devThis runs email dev --dir src/emails --port 3003. Open http://localhost:3003.