Email System
Learn how the automated email system works and how to customize email templates
The email system handles all automated transactional emails for your SaaS app. This guide covers how it works, how to customize email templates, and how to debug email delivery issues.
Prerequisites
Automated Emails Overview
The system automatically sends emails for key user and billing events. All emails are sent immediately (no queue) and logged to the database for tracking.
User Events
- â Welcome Email - When users sign up via OAuth
- đ§ Template:
welcome.mjml
Billing Events
- đŗ Order Confirmation - New subscription purchase
- â Payment Success - Recurring payment processed
- â Payment Failed - Payment issue notification
- đ Plan Changed - Upgrade/downgrade notification
- đĢ Subscription Cancelled - Cancellation confirmation
- đŗ Payment Method Updated - Card change confirmation
- â° Renewal Reminder - 7 days before renewal (via cron)
How Emails Are Triggered
- User registration: After successful OAuth sign-in (
src/lib/auth.ts) - Billing events: From Stripe/LemonSqueezy webhooks (
src/app/api/billing/*/webhook) - Scheduled events: Via cron jobs for renewal reminders
Email Template Structure
All email templates use MJML (Mailjet Markup Language), a responsive email framework that ensures your emails look great on all devices and email clients.
Directory Structure
đ templates/ - Email Templates
Each MJML file is a complete email template. Templates use variables (e.g., {{userName}}) that get replaced with actual data when the email is sent.
đ§Š partials/ - Reusable Components
Shared components like headers, footers, and buttons that are included in all templates. Edit once, update all emails.
đ¨ config/brand.json - Brand Configuration
Central place to customize colors, logo, company name, and other branding elements. Changes here apply to all emails.
Customizing Email Templates
The easiest way to customize emails is through the brand.json file. No code changes required!
Edit Brand Configuration
Open src/emails/config/brand.json and customize your branding:
1{2 "companyName": "Your SaaS Name",3 "companyAddress": "123 Main St, City, State 12345",4 "supportEmail": "support@yoursaas.com",5 "appUrl": "https://yoursaas.com",6 "logoUrl": "https://yoursaas.com/logo.png",7 "colors": {8 "primary": "#6366f1",9 "secondary": "#8b5cf6",10 "text": "#1f2937",11 "background": "#ffffff",12 "border": "#e5e7eb"13 },14 "social": {15 "twitter": "https://twitter.com/yoursaas",16 "linkedin": "https://linkedin.com/company/yoursaas",17 "github": "https://github.com/yoursaas"18 }19}What you can customize:
- Company Name - Appears in email headers and footers
- Logo URL - Your company logo (must be publicly accessible)
- Primary Color - Used for buttons and highlights
- Support Email - Where users can get help
- App URL - Link to your website/dashboard
- Social Links - Footer social media icons
Logo Requirements
- Is hosted on a public CDN (e.g., your website, Cloudinary, AWS S3)
- Has a transparent background (PNG format)
- Is at least 200px wide for clarity
- Works on both light and dark backgrounds
Edit Email Content (MJML Templates)
To change the actual email content, edit the MJML template files in src/emails/templates/
Example: Customizing the Welcome Email
Open src/emails/templates/welcome.mjml:
1<mjml>2 <mj-body>3 <mj-include path="../partials/header.mjml" />4 5 <mj-section>6 <mj-column>7 <mj-text font-size="24px" font-weight="bold">8 Welcome to {{companyName}}, {{userName}}! đ9 </mj-text>10 11 <mj-text>12 We're thrilled to have you on board. Your account is ready to go!13 </mj-text>14 15 <mj-include path="../partials/button.mjml">16 <mj-text href="{{actionUrl}}">Get Started</mj-text>17 </mj-include>18 </mj-column>19 </mj-section>20 21 <mj-include path="../partials/footer.mjml" />22 </mj-body>23</mjml>Available Variables:
Each template has specific variables you can use:
- Welcome:
{{userName}},{{actionUrl}} - Order Confirmation:
{{planName}},{{amount}},{{receiptUrl}} - Payment Success:
{{nextBillingDate}},{{amount}},{{planName}} - Payment Failed:
{{failureReason}},{{updatePaymentUrl}}
MJML Resources
- MJML Documentation
- MJML Live Editor - Test your changes live
Test Your Changes
After making changes, you can test emails without triggering actual events.
Option 1: Use Prisma Studio
- Run
npm run db:studio - Navigate to the EmailLog table
- View previously sent emails to see rendered output
Option 2: Trigger Real Events
- Welcome email: Create a new user account
- Order confirmation: Purchase a subscription
- Payment emails: Process a payment through Stripe/LemonSqueezy test mode
Changes Take Effect Immediately
brand.json take effect immediately - no server restart needed.Email Logging & Debugging
All sent emails are automatically logged to the EmailLog table in your database. This helps you track delivery, debug issues, and monitor email activity.
EmailLog Table Fields
- recipientEmail: Who the email was sent to
- templateType: Which template was used (e.g., "welcome", "payment_success")
- status: SENT or FAILED
- provider: Which email provider was used (resend, sendgrid, smtp)
- sentAt: Timestamp when email was sent
- failedAt: Timestamp if email failed
- error: Error message if delivery failed
- data: Template variables used (JSON)
View Email Logs
Open Prisma Studio to browse email history:
1npm run db:studioNavigate to the EmailLog table to see all sent emails, including success/failure status and error details.
Automatic Cleanup
Email logs older than EMAIL_RETENTION_DAYS (default: 90 days) are automatically deleted to keep your database clean. This runs via a cron job.
Debugging Failed Emails
- Check the EmailLog table for the error message
- Verify your environment variables are correct
- Check your email provider dashboard for delivery issues
- Make sure the recipient email is valid
- For Resend/SendGrid, check that your API key has send permissions
Email Provider Configuration
The email system supports multiple providers. You can switch providers by changing environment variables without modifying code.
Resend (Recommended)
1EMAIL_PROVIDER=resend2EMAIL_FROM=noreply@yourdomain.com3EMAIL_FROM_NAME=Your SaaS Name4RESEND_API_KEY=re_your_api_keyBest for: Easiest setup, generous free tier (3,000 emails/month), great DX
SendGrid
1EMAIL_PROVIDER=sendgrid2EMAIL_FROM=noreply@yourdomain.com3EMAIL_FROM_NAME=Your SaaS Name4SENDGRID_API_KEY=SG.your_api_keyBest for: Enterprise features, advanced analytics, higher volume
SMTP (Generic)
1EMAIL_PROVIDER=smtp2EMAIL_FROM=you@yourdomain.com3EMAIL_FROM_NAME=Your SaaS Name4SMTP_HOST=smtp.gmail.com5SMTP_PORT=5876SMTP_USER=your_email@gmail.com7SMTP_PASSWORD=your_app_password8SMTP_SECURE=falseBest for: Using existing email infrastructure, self-hosted servers
Provider Switching
- Update all relevant environment variables
- Restart your development server or redeploy
- Send a test email to verify the new provider works
- Check EmailLog table to confirm successful delivery
Adding Custom Email Templates
If you need to send emails for custom events (e.g., trial expiration, account deletion confirmation), you can create new templates and trigger them programmatically.
Create a New MJML Template
Create a new file in src/emails/templates/:
1<mjml>2 <mj-body>3 <mj-include path="../partials/header.mjml" />4 5 <mj-section>6 <mj-column>7 <mj-text font-size="24px" font-weight="bold">8 Your Trial is Expiring Soon9 </mj-text>10 11 <mj-text>12 Hi {{userName}},13 </mj-text>14 15 <mj-text>16 Your trial ends in {{daysRemaining}} days. Upgrade now to keep access!17 </mj-text>18 19 <mj-include path="../partials/button.mjml">20 <mj-text href="{{upgradeUrl}}">Upgrade Now</mj-text>21 </mj-include>22 </mj-column>23 </mj-section>24 25 <mj-include path="../partials/footer.mjml" />26 </mj-body>27</mjml>Define Template Type and Data Interface
Add your template type to src/lib/email/types.ts:
1// Add to the file2export interface TrialExpiringData {3 userName: string;4 daysRemaining: number;5 upgradeUrl: string;6}7 8// Update the EmailTemplateData union type9export type EmailTemplateData =10 | { templateType: 'welcome'; data: WelcomeEmailData }11 | { templateType: 'trial_expiring'; data: TrialExpiringData }12 // ... other template types13 ;Send the Custom Email
Use the email sending functions from anywhere in your code:
1import { sendEmail } from '@/lib/email/send';2import { renderTemplate } from '@/lib/email/render';3 4export async function POST() {5 const user = await getUserWithExpiringTrial();6 7 const html = await renderTemplate('trial_expiring', {8 userName: user.name,9 daysRemaining: 3,10 upgradeUrl: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`11 });12 13 await sendEmail({14 to: user.email,15 subject: 'Your Trial is Expiring Soon',16 html17 });18 19 return Response.json({ success: true });20}Email Sending Best Practices
- Always use descriptive subject lines
- Include unsubscribe links for marketing emails (not needed for transactional)
- Test templates thoroughly before sending to users
- Monitor EmailLog for delivery failures
- Keep email content concise and actionable
Troubleshooting Common Issues
MJML Compilation Errors
If you see MJML syntax errors when sending emails:
- Check that all MJML tags are properly closed
- Verify includes use correct paths (
../partials/header.mjml) - Test your MJML in the online editor
- Make sure variable names match (case-sensitive)
Variables Not Replaced
If you see {{userName}} in the email instead of the actual name:
- Check that you're passing the correct data object to
renderTemplate() - Verify variable names match exactly (case-sensitive)
- Look in EmailLog table to see what data was actually sent
Emails Look Broken in Gmail/Outlook
Email clients have different rendering engines. MJML helps with this, but:
- Test in multiple email clients before deploying
- Use MJML components (
mj-text,mj-button) instead of raw HTML - Avoid complex CSS - keep layouts simple
- Use Email on Acid or Litmus for testing
Delivery Rate Issues
If many emails end up in spam or aren't delivered:
- Verify your domain with your email provider (SPF, DKIM, DMARC records)
- Use a professional sender email (not gmail.com or hotmail.com)
- Avoid spam trigger words in subject lines
- Include an unsubscribe link for marketing emails
- Monitor your sender reputation in provider dashboard
Related Guides
Summary
You now understand how the email system works and how to customize it for your needs:
- âAutomated emails for registration, billing events, and renewals
- âEasy customization via brand.json for colors, logo, and company info
- âMJML templates for responsive, professional-looking emails
- âEmail logging for tracking delivery and debugging issues
- âMultiple providers supported (Resend, SendGrid, SMTP)