Launch NowDocs

Email

Send transactional email with Resend, build templates with React Email, preview them locally, and see every email the app sends.

Launch Now sends email through Resend. A single helper, sendEmail(), covers transactional messages; the sign-in code email is a React Email template that you can preview locally. This guide lists every email the app sends and shows how to add your own.

Where the code lives

FileRole
lib/email.tsThe Resend client (resend) and the sendEmail() helper.
emails/otp-email.tsxReact Email template for the one-time sign-in code.
emails/email-styles.tsxShared email theme: EmailStyles, defaultColors and the EmailClassNames / EmailColors types.
lib/auth.tsSends the sign-in code and organization invitations (Better Auth callbacks).
lib/account-actions.tsSends the "password set" confirmation.
lib/newsletter-actions.tsSends the newsletter confirmation email and newsletter broadcasts.
lib/notifications/server.tsSends notification emails in batches.

Setup

Create a Resend API key

Sign up at resend.com, verify the domain you send from, and create an API key.

Set the environment variables

.env
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxx
# Default sender, e.g. "Pulse <hello@example.com>"
EMAIL_FROM="Pulse <hello@example.com>"
VariableRequiredUsed for
RESEND_API_KEYYesAuthenticates every call to Resend.
EMAIL_FROMYesDefault sender of sendEmail(). Accepts a display name: "Pulse <hello@example.com>".
APP_NOREPLY_EMAILNoSender of the password confirmation and notification emails. Falls back to EMAIL_FROM.
APP_HELP_EMAILNoSupport address mentioned in the password confirmation email and on the legal pages.
APP_CONTACT_EMAILNoContact address shown on the legal pages.
RESEND_AUDIENCE_IDNoResend audience for the newsletter. The newsletter is off when it's unset.

All of them are validated by lib/env.ts. The optional APP_*_EMAIL variables must be plain email addresses (they're validated with z.email()), so they can't include a display name.

Good to know: .env.example also lists APP_ONBOARDING_EMAIL. It is validated but not used by any email yet.

Resend rejects emails sent from a domain you haven't verified. Verify the domain used in EMAIL_FROM (and APP_NOREPLY_EMAIL) before going live, or users won't receive their sign-in codes.

Send an email

sendEmail() takes a recipient, a subject and an HTML string. from is optional and defaults to EMAIL_FROM. It throws when Resend returns an error, so a failed send fails the calling action.

lib/email.ts
export const resend = new Resend(env.RESEND_API_KEY)
 
type SendEmailOptions = {
  to: string
  subject: string
  html: string
  from?: string
}
 
export async function sendEmail({ to, subject, html, from }: SendEmailOptions) {
  const { error } = await resend.emails.send({
    from: from ?? env.EMAIL_FROM,
    to,
    subject,
    html,
  })
 
  if (error) {
    console.error("Failed to send email:", error)
    throw new Error(error.message)
  }
}

Call it from server code only:

lib/account-actions.ts
await sendEmail({
  to: user.email,
  subject: "Password set successfully",
  html: `<p>Your password has been set successfully on ${siteConfig.name}.</p>...`,
  from: env.APP_NOREPLY_EMAIL ?? env.EMAIL_FROM,
})

For anything beyond emails.send (batches, audiences, broadcasts), use the exported resend client directly.

When you put user-provided values (names, organization names) into an HTML string, escape them first. lib/auth.ts, lib/newsletter-actions.ts and lib/notifications/server.ts each define a small escapeHtml() helper for this.

Emails the app sends

EmailTriggerWhereTemplate
Sign-in codeA user signs in or signs up with their email address.emailOTP plugin in lib/auth.tsemails/otp-email.tsx
Organization invitationAn owner or admin invites a member.organization plugin in lib/auth.tsInline HTML
Password setA user sets a password from the security settings or during two-factor setup.setPassword in lib/account-actions.tsInline HTML
Newsletter confirmationA visitor subscribes from the footer form.lib/newsletter-actions.tsInline HTML
Newsletter broadcastAn admin sends a newsletter.lib/newsletter-actions.ts (Resend broadcasts)Composed in the admin
NotificationA notification is sent and the recipient's email preference allows it.lib/notifications/server.tsInline HTML

The app has no password reset or email verification link emails: public email/password sign-up is disabled and users sign in with a one-time code or OAuth. See Authentication.

Sign-in code

The Better Auth emailOTP plugin calls sendVerificationOTP. The template is rendered to HTML with render() from @react-email/render:

lib/auth.ts
emailOTP({
  expiresIn: OTP_EXPIRES_IN_SECONDS,
  async sendVerificationOTP({ email, otp }) {
    const html = await render(
      OtpEmail({
        verificationCode: otp,
        email,
        appName: siteConfig.name,
        expirationMinutes: OTP_EXPIRES_IN_SECONDS / 60,
      })
    )
 
    await sendEmail({
      to: email,
      subject: "Your verification code",
      html,
    })
  },
}),

OTP_EXPIRES_IN_SECONDS is 600, so codes are valid for 10 minutes. The app name comes from lib/config/site-config.ts (Pulse in the demo; rename it for your product).

Organization invitation

sendInvitationEmail builds a link to /accept-invitation/<invitationId> from BETTER_AUTH_URL, with the organization name, the inviter's name and the expiry date. See Organizations.

Newsletter confirmation

Subscriptions are double opt-in. The confirmation email links to /api/newsletter/confirm with a signed token that expires after 48 hours. The same address gets at most one confirmation per minute. See Newsletter.

Notifications

lib/notifications/server.ts emails notifications with resend.batch.send, 100 emails per call. Each email greets the recipient, shows the title and body, adds an "Open" button when the notification has a link, and links to /account/notifications to manage preferences. See Notifications.

Templates

Templates live in emails/ and are React components built with @react-email/components. emails/otp-email.tsx shows the conventions to follow:

  • Styling with the React Email Tailwind component (pixelBasedPreset) and semantic class names: bg-background, bg-card, text-muted-foreground, border-border
  • <EmailStyles colors={colors} darkMode={darkMode} /> in the <Head> injects the CSS for those class names, in light mode and, when darkMode is true, under prefers-color-scheme: dark.
  • Props for customization: logoURL (a URL, or { light, dark } variants), colors, classNames, darkMode, poweredBy and localization (partial overrides of every text string).
  • OtpEmail.PreviewProps provides sample data for the preview server.

Change the colors

Edit defaultColors in emails/email-styles.tsx to match your brand, or pass colors for one email:

OtpEmail({
  verificationCode: otp,
  email,
  appName: siteConfig.name,
  colors: {
    light: { primary: "#4F46E5" },
    dark: { primary: "#A5B4FC" },
  },
})

Any color you don't set falls back to defaultColors.

Preview templates

pnpm email:dev

This runs email dev from the react-email package, a local preview server for the components in emails/. It renders each template with its PreviewProps and reloads when you edit them.

Add a template

Create the component

Add a file in emails/, for example emails/welcome-email.tsx. Start from otp-email.tsx: keep the Html, Head with EmailStyles, Tailwind and Body structure, and replace the content.

Add preview data

emails/welcome-email.tsx
WelcomeEmail.PreviewProps = {
  name: "Ada",
  appName: "Pulse",
} as WelcomeEmailProps
 
export default WelcomeEmail

Run pnpm email:dev to check the result.

Render and send it

import { render } from "@react-email/render"
import { WelcomeEmail } from "@/emails/welcome-email"
import { sendEmail } from "@/lib/email"
 
const html = await render(WelcomeEmail({ name: user.name, appName: siteConfig.name }))
await sendEmail({ to: user.email, subject: `Welcome to ${siteConfig.name}`, html })

The invitation, password and notification emails are plain HTML strings. To give them the same design as the sign-in code, move them to templates in emails/ and render them the same way.

Use another provider

Only lib/email.ts knows how the password confirmation, invitation and sign-in emails are delivered. To switch providers, rewrite sendEmail() with the same signature. The newsletter (lib/newsletter-actions.ts, features/newsletter/newsletter.server.ts) and notification emails (lib/notifications/server.ts) call the resend client directly and need their own changes.