TurboJet
Mail

Mail

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.

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):

OptionTypeNotes
emailstring[]Recipients
subjectstring
reactReactElementAn already-rendered template component
fromstring (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-up
  • EmailOtpSignIn.tsx — one-time code
  • components/Footer.tsx — shared footer; interpolates {email} into the message copy 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

  1. Add a .tsx file under packages/mail/src/emails (mirror an existing template’s copy prop).
  2. Export it from packages/mail/src/index.ts.
  3. Optionally add a subpath in packages/mail/package.json exports.
  4. Add copy keys in packages/i18n/src/mail/en.json and id.json.
  5. Call sendEmail with the component as react, passing locale and resolved copy.

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 → MagicLinkSignIn
  • emailOTP.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 dev

This runs email dev --dir src/emails --port 3003. Open http://localhost:3003.

On this page