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
| Var | Purpose |
|---|---|
RESEND_API_KEY | Your Resend API key |
EMAIL_FROM | Sender 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_KEYunset) → always a silent no-op + console warning, regardless ofcritical. Being unconfigured is a deployment choice, not a delivery failure. - Configured but the send actually fails → logged either way;
critical: trueadditionally 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.
- Unconfigured (
emails/— React Email templates, one file per email, each with a default export andPreviewPropsfor the dev preview server:verify-email.tsx,reset-password.tsx— the twocriticalones.welcome.tsx— sent on signup.subscription-started.tsx,subscription-canceled.tsx— sent on Stripe subscription lifecycle events.emails/components/layout.tsx— sharedEmailLayout/EmailButtonwrapper (brand header, footer, button styling). Plain inline styles — email clients don't runapp/globals.cssor Tailwind, so the brand color is hardcoded here rather than imported.
lib/auth.tswires the templates into better-auth:emailAndPassword.requireEmailVerification: true+sendResetPassword— password reset,critical: true.emailVerification.sendVerificationEmail— signup verification,critical: true.emailVerification.sendOnSignIn: truemeans 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
onCustomerCreatehook (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/onSubscriptionCancelsend the subscription-lifecycle emails — both look up the user's email bysubscription.referenceIdsince the Stripe plugin's callback data doesn't include it directly.
- Auth pages:
app/(auth)/forgot-password/page.tsx(callsauthClient.requestPasswordReset({email, redirectTo: "/reset-password"})) andapp/(auth)/reset-password/page.tsx(reads?token=from the redirect, callsauthClient.resetPassword({newPassword, token})). The login page shows a specific message and relies onsendOnSignInwhenerror.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:devstarts React Email's own dev server (defaulthttp://localhost:3030) with a live-reloading sidebar of every file inemails/. Needs@react-email/ui(a dev dependency, prompted for on first run if missing). - Different provider —
lib/email.tsis the only file that imports theresendpackage; swap the implementation insidesendEmail()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.