Skip to content

Email

Transactional emails are built with React Email and delivered through Resend. The apps/email/ workspace owns all templates and rendering – the API imports compiled templates and sends them via the Resend SDK.

Workspace Structure

bash
apps/email/
├── components/
   └── BaseTemplate.tsx       # Shared header, footer, and styling
├── templates/
   ├── otp-email.tsx          # OTP codes (sign-in, verification, password reset)
   ├── email-verification.tsx # Link-based email verification
   └── password-reset.tsx     # Link-based password reset
├── emails/                    # Preview files for dev server (sample data)
├── utils/
   └── render.ts              # renderEmailToHtml() / renderEmailToText()
├── index.ts                   # Public exports
└── package.json

Templates

Three templates ship out of the box, all wrapped in BaseTemplate for consistent branding:

TemplateUsed ByTrigger
OTPEmailEmail & OTP auth flowemailOTP plugin callback
EmailVerificationLink-based email verificationsendVerificationEmail()
PasswordResetPassword reset flowsendPasswordReset()

OTPEmail handles three types via a single type prop – "sign-in", "email-verification", and "forget-password" – each with different copy. Password resets include an additional security warning. The separate PasswordReset template uses a red button to emphasize the security-sensitive action.

Development

Preview templates locally with hot reload:

bash
bun email:dev

This starts the React Email preview server at http://localhost:3001. Files in emails/ provide sample data for each template – edit them to test different states.

TIP

The email workspace must be built before the API can import templates. The root bun dev handles this automatically, but if you run the API standalone, run bun email:build first.

Sending Emails

The API sends emails through helper functions in apps/api/lib/email.ts. Each helper renders a template to both HTML and plain text, then sends via Resend:

ts
// apps/api/lib/email.ts
import { OTPEmail, renderEmailToHtml, renderEmailToText } from "@repo/email";

const component = OTPEmail({
  otp,
  type,
  appName: env.APP_NAME,
  appUrl: env.APP_ORIGIN,
});
const html = await renderEmailToHtml(component);
const text = await renderEmailToText(component);

await sendEmail(env, {
  to: email,
  subject: `Your ${typeLabel} code`,
  html,
  text,
});

Available sender functions:

FunctionPurpose
sendOTP()OTP codes for all auth flows
sendVerificationEmail()Link-based email verification
sendPasswordReset()Password reset links
sendEmail()Low-level sender (validates recipients, requires plain text fallback for HTML)

WARNING

sendEmail() throws if you provide HTML without a plain text fallback. Always render both versions using renderEmailToHtml() and renderEmailToText().

Development Shortcut

In development, sendOTP() also prints the code to the terminal for convenience:

txt
OTP code for [email protected]: 482901

A valid RESEND_API_KEY is still required – the console output supplements the email, it doesn't replace it.

Adding a Template

  1. Create the template in apps/email/templates/:
tsx
// apps/email/templates/invitation.tsx
import { Button, Heading, Text } from "@react-email/components";
import { BaseTemplate } from "../components/BaseTemplate";

interface InvitationProps {
  inviterName: string;
  orgName: string;
  acceptUrl: string;
  appName?: string;
  appUrl?: string;
}

export function Invitation({
  inviterName,
  orgName,
  acceptUrl,
  appName,
  appUrl,
}: InvitationProps) {
  return (
    <BaseTemplate
      preview={`${inviterName} invited you to ${orgName}`}
      appName={appName}
      appUrl={appUrl}
    >
      <Heading
        style={{ fontSize: "24px", fontWeight: "600", margin: "0 0 24px" }}
      >
        You're invited
      </Heading>
      <Text style={{ fontSize: "16px", lineHeight: "24px" }}>
        {inviterName} invited you to join <strong>{orgName}</strong>.
      </Text>
      <Button href={acceptUrl}>Accept Invitation</Button>
    </BaseTemplate>
  );
}
  1. Export it from apps/email/index.ts:
ts
export { Invitation } from "./templates/invitation.js";
  1. Add a preview file in apps/email/emails/ with sample props for the dev server.

  2. Create a sender function in apps/api/lib/email.ts following the same render-then-send pattern.

Environment Variables

VariableRequiredDescription
RESEND_API_KEYFor emailResend API key (re_...)
RESEND_EMAIL_FROMFor emailSender address (e.g., [email protected])
APP_NAMENoUsed in email subject lines and branding (defaults to "Example")
APP_ORIGINYesUsed for links in email footer

Set in .env.local for development, Cloudflare secrets for staging/production. See Environment Variables.

File Map

LayerFiles
Templatesapps/email/templates/*.tsx
Shared layoutapps/email/components/BaseTemplate.tsx
Renderingapps/email/utils/render.ts
Sendingapps/api/lib/email.ts
Auth integrationemailOTP callback in apps/api/lib/auth.ts