Skip to content

Latest commit

Β 

History

History
474 lines (342 loc) Β· 11.8 KB

File metadata and controls

474 lines (342 loc) Β· 11.8 KB

Email System Documentation

Overview

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.


Quick Start

Development (Mock Mode)

No configuration needed! Emails are logged to console:

pnpm dev
# Register a user - check console for "verification email"

Production (Resend)

  1. Get API key from resend.com
  2. Add to .env:
    EMAIL_PROVIDER="resend"
    RESEND_API_KEY="re_xxxxx"
    EMAIL_FROM="noreply@yourdomain.com"
  3. Deploy - emails are sent automatically

Note: Setting EMAIL_PROVIDER="resend" automatically enables email verification in Better Auth (see src/lib/auth.ts).


Features Implemented

1. Email Verification

Flow:

  1. User registers β†’ Better Auth sends email with verification link (if EMAIL_PROVIDER="resend")
  2. User clicks link β†’ Email verified by Better Auth
  3. Account activated

Files:

  • Config: src/lib/auth.ts (Better Auth emailAndPassword.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 verify

2. Password Reset

Flow:

  1. User requests reset β†’ Better Auth sends email with reset link (1 hour expiry)
  2. User clicks link β†’ New password form
  3. Password updated via Better Auth

Files:

  • Config: src/lib/auth.ts (Better Auth emailAndPassword.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 password

Architecture

Directory Structure

src/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.ts contains sendVerificationEmail() and sendPasswordResetEmail()
  • These are called by Better Auth hooks (configured in src/lib/auth.ts)
  • Templates use Resend's @react-email/components for rendering

Better Auth Email Senders

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

Custom Email Sending

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} />,
});

Configuration

Environment Variables

# 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 notifications

Important: When EMAIL_PROVIDER="resend":

  • Email verification is automatically enabled in Better Auth
  • RESEND_API_KEY and EMAIL_FROM must be set
  • EMAIL_FROM must be from a verified domain in Resend

Provider Switching

Development:

EMAIL_PROVIDER="mock"
# or omit RESEND_API_KEY

Production:

EMAIL_PROVIDER="resend"
RESEND_API_KEY="re_xxxxx"

Creating New Email Templates

1. Create Template

// 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>
  );
}

2. Send Email

// 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}`);
  }
}

Available Components

Layout

<EmailLayout preview="Preview text">{/* Your content */}</EmailLayout>

Components

<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>

Security Features

Email Verification (Better Auth)

  • βœ… Managed by Better Auth verification table
  • βœ… Tokens expire based on Better Auth configuration
  • βœ… One-time use (deleted after verification)
  • βœ… Cryptographically secure tokens
  • βœ… Auto-enabled when EMAIL_PROVIDER="resend"

Password Reset (Better Auth)

  • βœ… 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)

General

  • βœ… All tokens stored in verification table (Better Auth)
  • βœ… Automatic cleanup of expired tokens
  • βœ… HTTPS-only links in production
  • βœ… Rate limiting built-in (Better Auth database-backed)

Troubleshooting

Emails not sending in production

Check:

  1. RESEND_API_KEY is set correctly
  2. EMAIL_FROM matches verified domain in Resend
  3. Check Resend dashboard for delivery status
  4. Check server logs for errors

Common issues:

  • Domain not verified in Resend
  • API key incorrect
  • FROM address doesn't match verified domain

Verification links not working

Check:

  1. BETTER_AUTH_URL is set correctly (not NEXTAUTH_URL)
  2. Token hasn't expired (configurable in Better Auth, default 1h for reset)
  3. Token hasn't been used already (deleted after use)
  4. Check database verification table (Better Auth table)
  5. Ensure EMAIL_PROVIDER="resend" if expecting real emails

Production Checklist

Before Launch

  • Get Resend API key
  • Verify domain in Resend
  • Set RESEND_API_KEY in environment
  • Set EMAIL_FROM to 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

Domain Verification (Resend)

  1. Add domain in Resend dashboard
  2. Add DNS records (SPF, DKIM, DMARC)
  3. Wait for verification (usually 5-10 min)
  4. Test sending from verified domain

Costs

Resend Pricing

  • 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

Future Enhancements

Ready to Implement

Templates are organized and ready for:

  1. Newsletter: Bulk sending, double opt-in
  2. Contact: Auto-reply to submissions
  3. Blog: Notify author when post published
  4. Users: Notify on role changes
  5. Profile: Verify email changes
  6. System: Onboarding, re-engagement, reports

Recommended Libraries

# 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)

Examples

Test in Development

# 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.

Preview Templates Locally

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>
  );
}

Support

Questions?

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