Setup Guide
Configure email providers for your environment
Put these in apps/web/.env.local (copied from apps/web/.env.example by pnpm run setup). packages/email/.env.example is reference only.
Schema lives in packages/email/keys.ts. Booleans (SMTP_SECURE, SMTP_POOL) use a stringBool transform — z.coerce.boolean() would treat "false" as true.
Common variables
# Shipped value in apps/web/.env.example
EMAIL_FROM="noreply@yourdomain.com"
# Optional. Unset = auto-detect (see below).
# EMAIL_PROVIDER="resend"
# EMAIL_PROVIDER="nodemailer"Never set EMAIL_PROVIDER=resend|nodemailer — that is not a valid enum value.
RESEND_API_KEY=re_xxxxxxxxxxxxx
EMAIL_FROM="noreply@yourdomain.com"Production EMAIL_FROM must be a domain you verified in Resend. An unverified from-address fails the send; Resend does not rewrite it to onboarding@resend.dev.
Auto-detect
When EMAIL_PROVIDER is unset:
| Condition | Provider |
|---|---|
NODE_ENV=production and RESEND_API_KEY is set | Resend |
| Anything else | Nodemailer |
A RESEND_API_KEY in pnpm dev does not select Resend. NODE_ENV=development is not its own branch.
Factory: packages/email/providers/index.ts (getProviderName(), getEmailProvider()).
What the app actually calls
Auth mail is triggered from Better Auth hooks in packages/auth/server.ts, not from app routes. Subscription mail is packages/auth/stripe-hooks.ts. See Templates for wired vs preview-only.
Troubleshooting
| Issue | Fix |
|---|---|
RESEND_API_KEY required | Set RESEND_API_KEY, or leave EMAIL_PROVIDER unset locally |
| Connection refused on port 1025 | docker compose up -d maildev |
| Emails not appearing | http://localhost:1080 |
| Invalid sender | Verify the domain in Resend; set EMAIL_FROM to that domain |