Transactional email system with React Email templates, multiple adapters, and real-time preview.
By the end of this guide, you'll have set up a comprehensive transactional email system with React Email templates, multiple provider adapters, and real-time preview capabilities for reliable email delivery in your SaaS application.
Overview
The SaaS Boilerplate includes a robust email system built on React Email that supports multiple delivery providers, template-based rendering, and real-time preview development. Key features include:
- Multi-provider support: SMTP, Resend, and other email providers via adapters
- React Email templates: Beautiful, responsive email templates with Tailwind CSS
- Type-safe templates: Zod schemas ensure data validation and type safety
- Real-time preview: Live email preview during development with
npm run dev:email - Notification integration: Seamless integration with the notification system for multi-channel delivery
- Template registry: Centralized template management with automatic rendering
- Error handling: Comprehensive error tracking and retry mechanisms
- Development tools: Hot-reload preview and template development workflow
The system integrates deeply with the notification service, allowing you to send emails as part of broader notification workflows while maintaining separation of concerns.
Architecture
Mail Provider System
The email system is built around the MailProvider class, which orchestrates template rendering, adapter selection, and delivery:
// src/@saas-boilerplate/providers/mail/mail.provider.tsx
const mailProvider = MailProvider.initialize({
secret: AppConfig.providers.mail.secret,
from: AppConfig.providers.mail.from,
adapter: getAdapter(AppConfig.providers.mail.provider),
templates: {
welcome: welcomeEmailTemplate,
'organization-invite': organizationInviteTemplate,
// ... more templates
}
})Email Adapters
The system supports multiple email providers through adapter pattern:
SMTP Adapter (for development/local):
// src/@saas-boilerplate/providers/mail/adapters/smtp.adapter.ts
export const smtpAdapter = MailProvider.adapter((options) => ({
send: async ({ to, subject, html, text }) => {
const transport = nodemailer.createTransporter(options.secret)
await transport.sendMail({ from: options.from, to, subject, html, text })
}
}))Resend Adapter (for production):
// src/@saas-boilerplate/providers/mail/adapters/resend.adapter.ts
export const resendAdapter = MailProvider.adapter((options) => ({
send: async ({ to, subject, html, text, scheduledAt }) => {
const resend = new Resend(options.secret)
await resend.emails.create({
from: options.from,
to,
subject,
html,
text,
scheduled_at: scheduledAt?.toISOString()
})
}
}))Template System
Templates are defined with Zod schemas and React components:
// Template definition with schema and component
export const welcomeEmailTemplate = MailProvider.template({
subject: `Welcome to ${AppConfig.name}`,
schema: z.object({
name: z.string().nullable().optional(),
email: z.string().email()
}),
render: WelcomeEmailComponent
})Notification Integration
The notification service integrates email delivery as a channel:
// src/services/notification.ts
channels: {
'email': {
send: async ({ data, template, context }) => {
// Find recipients and send emails using mail service
const recipients = await findRecipients(context)
await Promise.all(recipients.map(recipient =>
mail.send({
template: 'notification',
to: recipient.email,
data: { /* notification data */ }
})
))
}
}
}Setting Up Email
Configure Email Provider
Set up your email provider in the configuration. For development, use SMTP with MailHog:
// src/config/boilerplate.config.server.ts
export const AppConfig = {
providers: {
mail: {
provider: 'smtp',
secret: 'smtp://localhost:1025',
from: 'noreply@yourapp.com'
}
}
}For production, use Resend or another provider:
export const AppConfig = {
providers: {
mail: {
provider: 'resend',
secret: process.env.RESEND_API_KEY,
from: 'noreply@yourapp.com'
}
}
}Start MailHog for Development
For local development, run MailHog to capture emails:
# Using Docker (recommended)
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
# Or using the provided script
npm run docker:upAccess the MailHog web interface at http://localhost:8025 to view sent emails.
Create Email Templates
Create new email templates in src/content/mails/:
// src/content/mails/custom-email.template.ts
import { z } from 'zod'
import { MailProvider } from '@/@saas-boilerplate/providers/mail'
import { CustomEmailComponent } from './custom-email.component'
export const customEmailTemplate = MailProvider.template({
subject: 'Custom Email Subject',
schema: z.object({
userName: z.string(),
actionUrl: z.string().url()
}),
render: CustomEmailComponent
})Add the template to the mail service registry:
// src/services/mail.ts
export const mail = MailProvider.initialize({
// ... other config
templates: {
// ... existing templates
custom: customEmailTemplate
}
})Preview Templates in Development
Start the email preview server to see templates in real-time:
npm run dev:emailThis starts a development server at http://localhost:3001 where you can preview and test email templates with different data.
Backend Usage (Procedures & Controllers)
Sending Emails Directly
Use the mail service directly in your procedures and controllers:
// In a controller or procedure
import { mail } from '@/services/mail'
export const sendWelcomeEmail = igniter.mutation({
use: [AuthFeatureProcedure()],
handler: async ({ context, request }) => {
const session = await context.auth.getSession({
requirements: 'authenticated'
})
// Send welcome email
await mail.send({
template: 'welcome',
to: session.user.email,
data: {
name: session.user.name,
email: session.user.email
}
})
return response.success({ sent: true })
}
})Integration with Notifications (Recommended)
For better control and flexibility, integrate emails through the notification system:
// In procedures/controllers - use notification service instead
import { notification } from '@/services/notification'
export const userRegistered = igniter.procedure({
handler: async ({ context }) => {
// Send welcome notification (includes email + in-app)
await notification.send({
type: 'USER_REGISTERED',
context: {
recipientId: userId
},
data: {
userName: user.name,
welcomeUrl: '/app/dashboard'
}
})
}
})Template Data Validation
Templates validate data using Zod schemas:
// This will throw if data doesn't match schema
await mail.send({
template: 'welcome',
to: 'user@example.com',
data: {
name: 'John Doe', // ✅ Valid
email: 'john@example.com', // ✅ Valid
invalidField: 'not allowed' // ❌ Would cause validation error
}
})Frontend Usage (Client-side)
Email Preview Development
The email preview system allows you to develop templates interactively:
# Start preview server
npm run dev:email
# Visit http://localhost:3001 to see:
# - List of all email templates
# - Live preview with sample data
# - Hot-reload when templates change
# - Test different data scenariosTemplate Development Workflow
Develop email templates with React components:
// src/content/mails/custom-email.component.tsx
import * as ReactEmail from '@react-email/components'
export interface CustomEmailProps {
userName: string
actionUrl: string
}
export function CustomEmailComponent({ userName, actionUrl }: CustomEmailProps) {
return (
<ReactEmail.Html>
<ReactEmail.Body>
<ReactEmail.Text>Hello {userName}!</ReactEmail.Text>
<ReactEmail.Link href={actionUrl}>Click here</ReactEmail.Link>
</ReactEmail.Body>
</ReactEmail.Html>
)
}Email Templates
Available Templates
Prop
Type
Creating Custom Templates
Create new templates following the established pattern:
// 1. Define the React component
export function CustomTemplate({ data }: CustomTemplateProps) {
return (
<ReactEmail.Html>
{/* Your email HTML */}
</ReactEmail.Html>
)
}
// 2. Define the template with schema
export const customTemplate = MailProvider.template({
subject: 'Custom Subject',
schema: z.object({
requiredField: z.string(),
optionalField: z.string().optional()
}),
render: CustomTemplate
})
// 3. Register in mail service
templates: {
custom: customTemplate
}Practical Examples
Backend: User Registration Flow
Send welcome email when user registers:
// In user registration procedure
export const registerUser = igniter.procedure({
handler: async ({ context, request }) => {
const user = await createUser(request.body)
// Send welcome email via notification system
await notification.send({
type: 'USER_REGISTERED',
context: { recipientId: user.id },
data: {
userName: user.name,
email: user.email,
dashboardUrl: '/app/dashboard'
}
})
return response.created(user)
}
})Backend: Organization Invitation
Send invitation emails to new members:
// In organization invite procedure
export const inviteMember = igniter.procedure({
handler: async ({ context, request }) => {
const invitation = await createInvitation(request.body)
// Send invitation via direct mail (for one-off emails)
await mail.send({
template: 'organization-invite',
to: invitation.email,
data: {
organizationName: invitation.organization.name,
inviterName: session.user.name,
role: invitation.role,
inviteUrl: `/invite/${invitation.token}`
}
})
return response.success(invitation)
}
})Backend: Billing Notifications
Send billing-related emails through notifications:
// In billing webhook handler
export const handlePaymentSuccess = igniter.procedure({
handler: async ({ context, request }) => {
const payment = await processPayment(request.body)
await notification.send({
type: 'BILLING_SUCCESS',
context: { recipientId: payment.userId },
data: {
amount: payment.amount,
currency: payment.currency,
planName: payment.planName
}
})
}
})Email Data Structure
MailProviderSendParams
Prop
Type
Template Schema
Prop
Type
Troubleshooting
Best Practices
See Also
- Notifications - Multi-channel notification system that integrates with email
- Authentication & Sessions - User sessions and email verification flows
- Organizations and Tenancy - Organization-scoped email delivery
- Jobs & Queues - Background processing for bulk email operations
- Content Layer - MDX content system used for email templates
API Reference
Mail Service Methods
Prop
Type
Email Provider Configuration
Prop
Type
Development Commands
Prop
Type