Back to Help Center
Integrations

Email Setup

Email Setup & Customization Guide

This comprehensive guide covers setting up Resend for transactional emails and customizing email templates.

Table of Contents

  1. Important: Emails Are Not Auto-Triggered
  2. Initial Setup
  3. Customizing Email Templates
  4. Email Template Reference
  5. Previewing Emails
  6. Sending Emails
  7. Testing Emails
  8. Production Setup
  9. Wiring Up Automatic Emails
  10. Troubleshooting

Important: Emails Are Not Auto-Triggered

The email system is set up but not automatically wired to send emails. The templates and API endpoint exist, but you need to add the trigger points yourself.

What's included:

  • Email templates in /emails/
  • Resend client in /lib/resend.ts
  • Email API endpoint at /api/email

What you need to do:


Initial Setup

Step 1: Create a Resend Account

  1. Go to resend.com and click "Start for Free"
  2. Sign up with your email or GitHub account
  3. Verify your email address

Step 2: Get Your API Key

  1. In the Resend dashboard, click API Keys in the sidebar
  2. Click Create API Key
  3. Name it (e.g., "SaaS Boilerplate Development")
  4. Select permissions: Full Access for development, or Sending Access for production
  5. Click Create
  6. Copy the key immediately - it's only shown once!

Step 3: Add to Environment Variables

Add to your .env.local:

RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxx

Step 4: Test the Connection

Start your dev server and the console should show:

Resend initialized successfully

If you see "RESEND_API_KEY is not set", double-check your .env.local file.


Customizing Email Templates

Email templates are React components located in /emails/. They use React Email components.

Understanding the Template Structure

Every email template follows this structure:

// /emails/example.tsx

import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Hr,
  Html,
  Link,
  Preview,
  Section,
  Text,
} from '@react-email/components';
import * as React from 'react';

// Props interface - defines what data the email needs
interface ExampleEmailProps {
  userName?: string;
  actionUrl?: string;
}

// Main component
export default function ExampleEmail({
  userName = 'there',        // Default values for preview
  actionUrl = 'http://localhost:3000',
}: ExampleEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Preview text shown in email client list</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Hello {userName}!</Heading>
          <Text style={text}>Your email content here.</Text>
          <Button style={button} href={actionUrl}>
            Click Here
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

// Styles
const main = { backgroundColor: '#f6f9fc' };
const container = { /* ... */ };
const h1 = { /* ... */ };
const text = { /* ... */ };
const button = { /* ... */ };

Step-by-Step: Editing the Welcome Email

Let's customize the welcome email (/emails/welcome.tsx):

1. Open the file

// /emails/welcome.tsx

2. Change the company name

Find and replace "SaaS Boilerplate" with your app name:

// Before
<Heading style={h1}>Welcome to SaaS Boilerplate!</Heading>

// After
<Heading style={h1}>Welcome to YourAppName!</Heading>
// Before
<Preview>Welcome to SaaS Boilerplate - Let&apos;s get started!</Preview>

// After
<Preview>Welcome to YourAppName - Let&apos;s get started!</Preview>
// Before
<Text style={footer}>
  The SaaS Boilerplate Team
</Text>

// After
<Text style={footer}>
  The YourAppName Team
</Text>

3. Customize the welcome message

// Before
<Text style={text}>
  Thank you for signing up! We&apos;re excited to have you on board.
</Text>

// After
<Text style={text}>
  Thanks for joining YourAppName! You&apos;re now part of a community of
  10,000+ professionals improving their workflow.
</Text>

4. Update the getting started list

// Before
<ul style={list}>
  <li style={listItem}>Complete your profile</li>
  <li style={listItem}>Explore your dashboard</li>
  <li style={listItem}>Check out our pricing plans</li>
</ul>

// After
<ul style={list}>
  <li style={listItem}>Complete your profile in Settings</li>
  <li style={listItem}>Create your first project</li>
  <li style={listItem}>Invite your team members</li>
  <li style={listItem}>Check out our quick start guide</li>
</ul>

5. Change the button text

// Before
<Button style={button} href={loginUrl}>
  Go to Dashboard
</Button>

// After
<Button style={button} href={loginUrl}>
  Start Your First Project
</Button>

6. Update footer links

// Before
<Text style={footerLinks}>
  <Link href="#" style={link}>Website</Link>
  {' • '}
  <Link href="#" style={link}>Documentation</Link>
  {' • '}
  <Link href="#" style={link}>Support</Link>
</Text>

// After
<Text style={footerLinks}>
  <Link href="https://yourapp.com" style={link}>Website</Link>
  {' • '}
  <Link href="https://yourapp.com/docs" style={link}>Help Center</Link>
  {' • '}
  <Link href="mailto:support@yourapp.com" style={link}>Contact Us</Link>
</Text>

7. Change colors (optional)

Update the style objects at the bottom of the file:

// Change primary button color
const button = {
  backgroundColor: '#4F46E5',  // Change to your brand color
  borderRadius: '6px',
  color: '#fff',
  // ...
};

// Change heading color
const h1 = {
  color: '#4F46E5',  // Match your brand
  // ...
};

Adding a Logo to Emails

import { Img } from '@react-email/components';

// In your template
<Container style={container}>
  <Img
    src="https://yourapp.com/images/logo.png"
    width="150"
    height="40"
    alt="YourApp Logo"
    style={{ margin: '0 auto 24px' }}
  />
  <Heading style={h1}>Welcome!</Heading>
  {/* ... */}
</Container>

Important: Use a full URL (https://...) for images in emails, not relative paths. Images should be hosted on your production domain.


Email Template Reference

Available Templates

| Template | File | Triggered By | |----------|------|-------------| | Welcome | /emails/welcome.tsx | User signup | | Subscription Confirmed | /emails/subscription-confirmed.tsx | Successful payment | | Subscription Canceled | /emails/subscription-canceled.tsx | Subscription cancellation | | Payment Failed | /emails/payment-failed.tsx | Failed payment attempt |

Welcome Email (welcome.tsx)

Purpose: Sent immediately after a user signs up.

Variables: | Variable | Type | Description | |----------|------|-------------| | userName | string | User's name (or "there" as fallback) | | loginUrl | string | URL to the login/dashboard page |

Customization ideas:

  • Add a promotional offer for new users
  • Include links to getting started resources
  • Add social proof (user count, testimonials)

Subscription Confirmed (subscription-confirmed.tsx)

Purpose: Sent when a user successfully subscribes to a paid plan.

Variables: | Variable | Type | Description | |----------|------|-------------| | userName | string | User's name | | planName | string | Name of the subscribed plan | | amount | string | Formatted amount (e.g., "$29") | | billingPeriod | string | "month" or "year" | | dashboardUrl | string | URL to the dashboard |

Customization ideas:

  • Add next steps for getting started with their plan
  • Include key features of their plan
  • Offer onboarding call for higher tiers

Subscription Canceled (subscription-canceled.tsx)

Purpose: Sent when a user cancels their subscription.

Variables: | Variable | Type | Description | |----------|------|-------------| | userName | string | User's name | | planName | string | Name of the canceled plan | | endDate | string | When access ends | | resubscribeUrl | string | URL to pricing page |

Customization ideas:

  • Add a win-back offer/discount
  • Ask for cancellation feedback
  • Remind them of what they'll lose

Payment Failed (payment-failed.tsx)

Purpose: Sent when a recurring payment fails.

Variables: | Variable | Type | Description | |----------|------|-------------| | userName | string | User's name | | planName | string | Name of the plan | | amount | string | Failed payment amount | | updatePaymentUrl | string | URL to billing page |

Customization ideas:

  • Create urgency (grace period details)
  • Explain what happens if not resolved
  • Provide multiple payment update options

Previewing Emails

Method 1: React Email Dev Server (Recommended)

  1. Install React Email globally:
npm install -g react-email
  1. Start the preview server:
cd /path/to/your/project
npx react-email dev --dir emails
  1. Open http://localhost:3000 in your browser

  2. Select an email template to preview

  3. Edit the template file - changes appear instantly!

Method 2: Send a Test Email

Send a test email to yourself via the API:

// Create a test script: /scripts/test-email.ts
import { Resend } from 'resend';
import WelcomeEmail from '../emails/welcome';

const resend = new Resend(process.env.RESEND_API_KEY);

async function sendTestEmail() {
  const { data, error } = await resend.emails.send({
    from: 'onboarding@resend.dev',  // Use your domain in production
    to: 'your-email@example.com',
    subject: 'Test Welcome Email',
    react: WelcomeEmail({
      userName: 'Test User',
      loginUrl: 'http://localhost:3000/dashboard',
    }),
  });

  if (error) {
    console.error('Error:', error);
  } else {
    console.log('Email sent:', data);
  }
}

sendTestEmail();

Run with:

npx ts-node scripts/test-email.ts

Sending Emails

From API Routes

// In any API route
import { sendEmail } from '@/lib/resend';
import WelcomeEmail from '@/emails/welcome';

await sendEmail({
  to: user.email,
  subject: 'Welcome to YourApp!',
  react: WelcomeEmail({
    userName: user.name,
    loginUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
  }),
});

From Stripe Webhooks

Emails are typically triggered from webhook handlers:

// In /app/api/stripe/webhooks/route.ts

// After successful payment:
await sendEmail({
  to: userEmail,
  subject: 'Your subscription is active!',
  react: SubscriptionConfirmedEmail({
    userName: userName,
    planName: 'Pro',
    amount: '$29',
    billingPeriod: 'month',
    dashboardUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
  }),
});

Testing Emails

⚠️ Important: Domain Verification Required

Without a verified domain, Resend only allows sending emails to your own Resend account email address. To send to other recipients:

  1. Verify a domain in Resend Dashboard → Domains
  2. Update EMAIL_FROM to use your verified domain

For testing without a verified domain, use EMAIL_FROM=onboarding@resend.dev and send only to your Resend account email.

Test Email Addresses

With verified domain: You can send to any email address:

  • Your personal email
  • test+variant1@youremail.com (Gmail plus addressing)
  • Use Mailinator for disposable addresses

Without verified domain: You can only send to your Resend account email address.

Check Email Delivery

In the Resend dashboard:

  1. Go to Emails in the sidebar
  2. See delivery status for each email
  3. Click an email for detailed delivery info

Test Different Scenarios

Create test data for each email:

// Test welcome email
WelcomeEmail({ userName: 'John' })
WelcomeEmail({ userName: undefined })  // Test fallback

// Test payment failed
PaymentFailedEmail({
  userName: 'John',
  planName: 'Pro',
  amount: '$29',
})

Production Setup

Step 1: Verify Your Domain

  1. In Resend dashboard, go to Domains
  2. Click Add Domain
  3. Enter your domain (e.g., yourapp.com)
  4. Add the DNS records Resend provides:

| Type | Name | Value | |------|------|-------| | TXT | @ | v=spf1 include:_spf.resend.com ~all | | TXT | resend._domainkey | (Provided by Resend) | | TXT | _dmarc | v=DMARC1; p=none; |

  1. Wait for DNS propagation (usually 5-30 minutes)
  2. Click Verify

Step 2: Update Environment Variables

# Production .env
EMAIL_FROM="YourApp <noreply@yourapp.com>"
EMAIL_REPLY_TO=support@yourapp.com

Step 3: Update Email Templates

Ensure all templates use your domain for:

  • Logo images
  • Links
  • Company name
  • Reply-to address

Step 4: Test Production Emails

  1. Deploy to staging environment
  2. Test signup flow → Welcome email
  3. Test payment flow → Subscription confirmed email
  4. Test cancellation → Cancellation email
  5. Verify all links work

React Email Component Reference

Basic Components

import {
  Html,      // Root element
  Head,      // Document head
  Body,      // Email body
  Container, // Centered container
  Section,   // Section wrapper
  Text,      // Paragraph text
  Heading,   // H1-H6 headings
  Link,      // Anchor links
  Button,    // CTA buttons
  Img,       // Images
  Hr,        // Horizontal rule
  Preview,   // Preview text (shows in email list)
} from '@react-email/components';

Usage Examples

// Text paragraph
<Text style={{ fontSize: '16px', color: '#333' }}>
  Your paragraph content here.
</Text>

// Button with link
<Button
  href="https://yourapp.com"
  style={{
    backgroundColor: '#000',
    color: '#fff',
    padding: '12px 24px',
    borderRadius: '6px',
  }}
>
  Click Here
</Button>

// Image (must use absolute URL)
<Img
  src="https://yourapp.com/logo.png"
  width="150"
  height="40"
  alt="Logo"
/>

// Link
<Link href="https://yourapp.com" style={{ color: '#0066cc' }}>
  Visit our website
</Link>

Rate Limits

| Plan | Daily Limit | Monthly Limit | |------|-------------|---------------| | Free | 100 emails | 3,000 emails | | Pro | 1,000+ | Based on plan |

Monitor your usage in the Resend dashboard.


Troubleshooting

Emails not being sent

Check 1: Is RESEND_API_KEY set?

# In your terminal
echo $RESEND_API_KEY

Check 2: Is the API key valid?

  • Go to Resend dashboard → API Keys
  • Ensure the key is active

Check 3: Check Resend logs

  • Dashboard → Emails → Look for errors

Emails going to spam

Solution 1: Verify your domain

  • Set up SPF, DKIM, and DMARC records

Solution 2: Improve email content

  • Avoid spam trigger words
  • Include unsubscribe link
  • Use proper from address

Solution 3: Warm up your domain

  • Start with low volume
  • Gradually increase sending

Template not rendering correctly

Check 1: Valid JSX syntax?

  • Look for unclosed tags
  • Check for missing imports

Check 2: Styles applied correctly?

  • Use inline styles (not CSS classes)
  • Check style object syntax

Check 3: Variables passed correctly?

  • Console.log the props
  • Check for undefined values

Images not appearing

Problem: Using relative URLs

// Wrong
<Img src="/images/logo.png" />

// Right
<Img src="https://yourapp.com/images/logo.png" />

Problem: Image not publicly accessible

  • Ensure image is in /public folder
  • Deploy before testing production emails

"From" address errors

Problem: Sending from unverified domain

  • Verify your domain in Resend
  • Or use onboarding@resend.dev for testing

Wiring Up Automatic Emails

To have emails sent automatically, you need to add sendEmail() calls at the appropriate trigger points.

Send Welcome Email on Signup

Add to your signup success handler or create a Supabase database trigger:

// Option 1: In your signup API route or after successful signup
import { sendEmail } from '@/lib/resend';
import WelcomeEmail from '@/emails/welcome';

// After user is created
await sendEmail({
  to: user.email,
  subject: 'Welcome to YourApp!',
  react: WelcomeEmail({
    userName: user.name || user.email,
    loginUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
  }),
});

Send Subscription Emails from Webhooks

Add to /app/api/stripe/webhooks/route.ts:

import { sendEmail } from '@/lib/resend';
import SubscriptionConfirmedEmail from '@/emails/subscription-confirmed';
import SubscriptionCanceledEmail from '@/emails/subscription-canceled';
import PaymentFailedEmail from '@/emails/payment-failed';

// In handleSubscriptionChange function, after upserting subscription:
if (subscription.status === 'active') {
  // Get user email from profile
  const { data: profile } = await getSupabaseAdmin()
    .from('profiles')
    .select('email, full_name')
    .eq('id', userId)
    .single();

  if (profile?.email) {
    await sendEmail({
      to: profile.email,
      subject: 'Your subscription is now active!',
      react: SubscriptionConfirmedEmail({
        userName: profile.full_name || 'there',
        planName: 'Pro', // Get from price lookup
        amount: '$29',
        billingPeriod: 'month',
        dashboardUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
      }),
    });
  }
}

// In handleSubscriptionDeleted or when cancel_at is set:
// Send SubscriptionCanceledEmail

// In handleInvoicePaymentFailed:
// Send PaymentFailedEmail

Using the Email API Endpoint

Alternatively, call the email API endpoint from anywhere:

await fetch('/api/email', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    // Include if INTERNAL_API_KEY is set:
    // 'Authorization': `Bearer ${process.env.INTERNAL_API_KEY}`
  },
  body: JSON.stringify({
    to: 'user@example.com',
    template: 'welcome', // or 'subscription-confirmed', 'subscription-canceled', 'payment-failed'
    data: {
      userName: 'John',
      // ... other template-specific data
    },
  }),
});

Email Files Quick Reference

| File | Purpose | |------|---------| | /emails/welcome.tsx | Welcome email template | | /emails/subscription-confirmed.tsx | Payment success template | | /emails/subscription-canceled.tsx | Cancellation template | | /emails/payment-failed.tsx | Failed payment template | | /lib/resend.ts | Resend client & sendEmail function | | /app/api/email/route.ts | Email API endpoint |