Templator includes a complete, production-ready email system powered by Better Auth with:
- β Email verification for new accounts (auto-enabled with Resend)
- β Password reset functionality (built-in)
- β Mock mode for development (zero configuration)
- β Resend integration for production
- β React Email templates (type-safe, maintainable)
- β Edge-compatible (Cloudflare Workers ready)
Integration: Email flows are handled by Better Auth with custom email sending via Resend.
No configuration needed! Emails are logged to console:
pnpm dev
# Register a user - check console for "verification email"- Get API key from resend.com
- Add to
.env:EMAIL_PROVIDER="resend" RESEND_API_KEY="re_xxxxx" EMAIL_FROM="noreply@yourdomain.com"
- Deploy - emails are sent automatically
Note: Setting EMAIL_PROVIDER="resend" automatically enables email verification in Better Auth (see src/lib/auth.ts).
Flow:
- User registers β Better Auth sends email with verification link (if EMAIL_PROVIDER="resend")
- User clicks link β Email verified by Better Auth
- Account activated
Files:
- Config:
src/lib/auth.ts(Better AuthemailAndPassword.sendVerificationEmail) - Email sender:
src/lib/emails/auth-emails.ts(sendVerificationEmail()) - Template:
src/lib/emails/templates/auth/verify-email.tsx - Page:
src/app/verify-email/page.tsx
Auto-enabled: Email verification is automatically enabled when EMAIL_PROVIDER="resend" (configured in src/lib/auth.ts).
Test:
# Register new user
# Check console logs for verification link
# Click link to verifyFlow:
- User requests reset β Better Auth sends email with reset link (1 hour expiry)
- User clicks link β New password form
- Password updated via Better Auth
Files:
- Config:
src/lib/auth.ts(Better AuthemailAndPassword.sendResetPassword) - Email sender:
src/lib/emails/auth-emails.ts(sendPasswordResetEmail()) - Template:
src/lib/emails/templates/auth/password-reset.tsx - Pages:
src/app/forgot-password/page.tsx(request reset form)src/app/reset-password/page.tsx(reset form with token in URL)
Built-in: Password reset is always available, regardless of EMAIL_PROVIDER (but emails only sent when not in mock mode).
Test:
# Go to /login β "Forgot password?"
# Enter email
# Check console for reset link
# Follow link to reset passwordsrc/lib/emails/
βββ auth-emails.ts # Better Auth email senders
βββ templates/
βββ base/
β βββ layout.tsx # Shared email layout
β βββ components.tsx # Reusable components
βββ auth/
β βββ verify-email.tsx # Email verification template
β βββ password-reset.tsx # Password reset template
βββ users/ # Ready for role notifications
βββ blog/ # Ready for post notifications
βββ newsletter/ # Ready for newsletter
βββ contact/ # Ready for contact auto-reply
βββ profile/ # Ready for email change
βββ system/ # Ready for system emails
Better Auth Integration:
auth-emails.tscontainssendVerificationEmail()andsendPasswordResetEmail()- These are called by Better Auth hooks (configured in
src/lib/auth.ts) - Templates use Resend's
@react-email/componentsfor rendering
Auth emails are sent through Better Auth hooks:
// src/lib/auth.ts
emailAndPassword: {
sendVerificationEmail: async ({ user, url }) => {
await sendVerificationEmail({ user, url });
},
sendResetPassword: async ({ user, url }) => {
await sendPasswordResetEmail({ user, url });
},
}Automatic behavior:
- Mock mode (
EMAIL_PROVIDER="mock") β Logs to console - Production (
EMAIL_PROVIDER="resend") β Sends via Resend - Email verification auto-enabled with Resend
- Type-safe React templates
- Error handling included
For non-auth emails (newsletter, notifications, etc.), use Resend directly:
import { Resend } from 'resend';
import { MyEmailTemplate } from '@/lib/emails/templates/my-email';
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: process.env.EMAIL_FROM!,
to: user.email,
subject: 'Your subject',
react: <MyEmailTemplate {...props} />,
});# Email Provider (controls Better Auth email verification)
EMAIL_PROVIDER="mock" # "mock" (dev) or "resend" (production)
# Resend Configuration (required if EMAIL_PROVIDER="resend")
RESEND_API_KEY="re_xxxxx" # Get from resend.com
EMAIL_FROM="noreply@yourdomain.com" # Sender address (must match verified domain)
# Optional
EMAIL_REPLY_TO="support@yourdomain.com" # Reply-to address
ADMIN_EMAIL="admin@yourdomain.com" # For admin notificationsImportant: When EMAIL_PROVIDER="resend":
- Email verification is automatically enabled in Better Auth
RESEND_API_KEYandEMAIL_FROMmust be setEMAIL_FROMmust be from a verified domain in Resend
Development:
EMAIL_PROVIDER="mock"
# or omit RESEND_API_KEYProduction:
EMAIL_PROVIDER="resend"
RESEND_API_KEY="re_xxxxx"// src/lib/emails/templates/users/role-changed.tsx
import { EmailLayout } from "../base/layout";
import { Heading, Paragraph, Code } from "../base/components";
interface RoleChangedProps {
name: string;
oldRole: string;
newRole: string;
}
export function RoleChangedTemplate({ name, oldRole, newRole }: RoleChangedProps) {
return (
<EmailLayout preview="Your role has been updated">
<Heading>Role Updated</Heading>
<Paragraph>Hi {name},</Paragraph>
<Paragraph>
Your role has been changed from <Code>{oldRole}</Code> to <Code>{newRole}</Code>.
</Paragraph>
</EmailLayout>
);
}// src/features/users/actions.ts
import { Resend } from 'resend';
import { RoleChangedTemplate } from '@/lib/emails/templates/users/role-changed';
export async function updateUserRole(userId: string, newRole: string) {
// ... update logic ...
// Send email via Resend
if (process.env.EMAIL_PROVIDER === "resend") {
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: process.env.EMAIL_FROM!,
to: user.email,
subject: 'Your role has been updated',
react: <RoleChangedTemplate
name={user.name}
oldRole={user.role}
newRole={newRole}
/>,
});
} else {
// Mock mode - log to console
console.log(`π§ [MOCK EMAIL] Role changed: ${user.email}`);
}
}<EmailLayout preview="Preview text">{/* Your content */}</EmailLayout><Heading>Main Title</Heading>
<Paragraph>Regular text content</Paragraph>
<Button href="https://...">Click Here</Button>
<Code>code-or-token</Code>
<Alert>Important notice</Alert>
<Divider />
<Small>Secondary information</Small>- β
Managed by Better Auth
verificationtable - β Tokens expire based on Better Auth configuration
- β One-time use (deleted after verification)
- β Cryptographically secure tokens
- β
Auto-enabled when
EMAIL_PROVIDER="resend"
- β Tokens expire in 1 hour (Better Auth default)
- β One-time use (deleted after reset)
- β Doesn't reveal if email exists (security best practice)
- β Old password immediately invalidated
- β Uses custom PBKDF2 hashing (Cloudflare Workers compatible)
- β
All tokens stored in
verificationtable (Better Auth) - β Automatic cleanup of expired tokens
- β HTTPS-only links in production
- β Rate limiting built-in (Better Auth database-backed)
Check:
RESEND_API_KEYis set correctlyEMAIL_FROMmatches verified domain in Resend- Check Resend dashboard for delivery status
- Check server logs for errors
Common issues:
- Domain not verified in Resend
- API key incorrect
- FROM address doesn't match verified domain
Check:
BETTER_AUTH_URLis set correctly (notNEXTAUTH_URL)- Token hasn't expired (configurable in Better Auth, default 1h for reset)
- Token hasn't been used already (deleted after use)
- Check database
verificationtable (Better Auth table) - Ensure
EMAIL_PROVIDER="resend"if expecting real emails
- Get Resend API key
- Verify domain in Resend
- Set
RESEND_API_KEYin environment - Set
EMAIL_FROMto verified domain - Set
EMAIL_PROVIDER=resend - Test email verification flow
- Test password reset flow
- Check spam folder (adjust SPF/DKIM if needed)
- Monitor Resend dashboard for deliverability
- Add domain in Resend dashboard
- Add DNS records (SPF, DKIM, DMARC)
- Wait for verification (usually 5-10 min)
- Test sending from verified domain
- Free tier: 100 emails/day, 3,000/month
- Pro plan: $20/month for 50,000 emails
- Growth plan: $80/month for 500,000 emails
Estimated for typical SaaS:
- 100 users/month Γ 2 emails (verification + welcome) = 200 emails
- 20 password resets/month = 40 emails
- Total: ~250 emails/month = FREE
Templates are organized and ready for:
- Newsletter: Bulk sending, double opt-in
- Contact: Auto-reply to submissions
- Blog: Notify author when post published
- Users: Notify on role changes
- Profile: Verify email changes
- System: Onboarding, re-engagement, reports
# Background jobs (Cloudflare Queues)
pnpm add @cloudflare/workers-types
# Email list management
# Use Resend's audiences feature
# Advanced templates
# Use @react-email/components (already installed)# Start dev server
pnpm dev
# Register a user via UI at /register
# Or use Better Auth API directly:
curl -X POST http://localhost:3000/api/auth/sign-up/email \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@example.com","password":"password123"}'
# Check terminal logs for:
π§ [MOCK EMAIL] Verification email would be sent to: test@example.com
Link: http://localhost:3000/verify-email?token=...Note: In mock mode (EMAIL_PROVIDER="mock"), emails are logged to console with verification links you can click.
Create src/emails-preview.tsx:
import { VerifyEmailTemplate } from "./lib/emails/templates/auth/verify-email";
export default function EmailPreview() {
return (
<div>
<h1>Email Templates Preview</h1>
<VerifyEmailTemplate name="John Doe" verificationUrl="https://example.com/verify/token123" />
</div>
);
}Questions?
- Check Resend docs
- Check React Email docs
- Review code examples in
src/lib/emails/
Common patterns:
- All templates in
templates/folder - All actions in feature
actions.ts - Use
sendEmail()function for all sends - Mock mode for development, Resend for production