Live demo — shared accounts, sample data, reset regularly. Don't enter anything real.Get the source

Transactional Email

Transactional Email

What it does

Sends verification, password-reset, welcome, and subscription-lifecycle emails via Resend, rendered with React Email components. Email is now required to complete signup — requireEmailVerification: true blocks sign-in until the address is verified.

How to configure it

VarPurpose
RESEND_API_KEYYour Resend API key
EMAIL_FROMSender address, e.g. Launchpad <noreply@yourdomain.com>. Falls back to Resend's shared onboarding@resend.dev if unset (fine for testing, not for production — Resend rate-limits and may flag that address)

Without RESEND_API_KEY, every send is a no-op: lib/email.ts logs ⚠️ [email] RESEND_API_KEY not set — skipping "<subject>" to <recipient> and returns — the app keeps working, including signup (users land on a "check your email" screen; they just won't receive anything until the key is set). This is the same degrade-gracefully contract as every other optional integration in this starter.

How it works

  • lib/email.ts — sendEmail({to, subject, react, critical?}), a thin wrapper around the Resend SDK.
    • Unconfigured (RESEND_API_KEY unset) → always a silent no-op + console warning, regardless of critical. Being unconfigured is a deployment choice, not a delivery failure.
    • Configured but the send actually fails → logged either way; critical: true additionally re-throws, so the caller (verification, password reset — both required for the user to do anything) can surface it. Everything else (welcome, subscription emails) stays best-effort.
  • emails/ — React Email templates, one file per email, each with a default export and PreviewProps for the dev preview server:
    • verify-email.tsx, reset-password.tsx — the two critical ones.
    • welcome.tsx — sent on signup.
    • subscription-started.tsx, subscription-canceled.tsx — sent on Stripe subscription lifecycle events.
    • emails/components/layout.tsx — shared EmailLayout/EmailButton wrapper (brand header, footer, button styling). Plain inline styles — email clients don't run app/globals.css or Tailwind, so the brand color is hardcoded here rather than imported.
  • lib/auth.ts wires the templates into better-auth:
    • emailAndPassword.requireEmailVerification: true + sendResetPassword — password reset, critical: true.
    • emailVerification.sendVerificationEmail — signup verification, critical: true. emailVerification.sendOnSignIn: true means a user who lost the first email automatically gets a fresh one on their next blocked login attempt, instead of being stuck.
    • The Stripe plugin's onCustomerCreate hook (fires when a Stripe customer is created for a new signup, the closest available hook to "user finished signing up" that already hands us their email) sends the welcome email.
    • subscription.onSubscriptionComplete / onSubscriptionCancel send the subscription-lifecycle emails — both look up the user's email by subscription.referenceId since the Stripe plugin's callback data doesn't include it directly.
  • Auth pages: app/(auth)/forgot-password/page.tsx (calls authClient.requestPasswordReset({email, redirectTo: "/reset-password"})) and app/(auth)/reset-password/page.tsx (reads ?token= from the redirect, calls authClient.resetPassword({newPassword, token})). The login page shows a specific message and relies on sendOnSignIn when error.code === "EMAIL_NOT_VERIFIED"; the signup page shows a "check your email" screen instead of redirecting when the response has no session token (which is what happens now that verification is required).

How to extend it

  • Add a new transactional email — see recipes/add-an-email-template.md.
  • Preview templates while developing: npm run email:dev starts React Email's own dev server (default http://localhost:3030) with a live-reloading sidebar of every file in emails/. Needs @react-email/ui (a dev dependency, prompted for on first run if missing).
  • Different provider — lib/email.ts is the only file that imports the resend package; swap the implementation inside sendEmail() and every call site (lib/auth.ts, future callers) stays the same.

How to remove it

Set emailAndPassword.requireEmailVerification: false and delete the sendResetPassword/sendVerificationEmail/onCustomerCreate/ onSubscriptionComplete/onSubscriptionCancel email-sending calls in lib/auth.ts (keep the hooks themselves if you use them for anything else). Delete lib/email.ts, emails/, app/(auth)/forgot-password/, app/(auth)/reset-password/, the "Forgot password?" link on app/(auth)/login/page.tsx, and the resend/@react-email/components/ react-email/@react-email/ui dependencies.