React Email11 min read

The Complete SaaS Email Stack: Build Every Template with React Email and Resend

Build the six essential SaaS emails - welcome, verification, password reset, trial ending, receipt, and dunning - with React Email and Resend. Includes setup code, edge cases, and time-saving shortcuts.

R

React Emails Pro

April 2, 2026

Every SaaS needs the same six emails before launch day: welcome, email verification, password reset, trial ending, payment receipt, and failed payment recovery. Skip any one of them and you get support tickets, churn, or both.

This guide walks through building each one with React Email and Resend in a Next.js app. For each template, you will see the setup code, the edge cases that eat your time, and exactly how long it takes to build from scratch versus dropping in a production-ready template.

EmailWhen It SendsWhat Breaks If You Skip It
WelcomeImmediately after signupUsers forget your product exists within 24 hours
Email verificationAfter signup or email changeFake accounts, deliverability damage from bad addresses
Password resetUser-initiatedSupport tickets, account lockouts, users leave
Trial ending3 days and 1 day before expiryUsers churn without ever seeing the upgrade prompt
Payment receiptAfter successful chargeRefund requests, compliance issues, lost trust
Failed paymentAfter charge failureSilent involuntary churn - your biggest revenue leak

The six emails every SaaS needs before launch


Project Setup: React Email + Resend

Before building templates, set up the shared infrastructure. This takes about 10 minutes and every template reuses the same pattern.

terminal
npm install @react-email/components resend
npm install -D react-email
lib/email.ts
import { Resend } from "resend";

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

// Shared send helper with error handling
export async function sendEmail({
  to,
  subject,
  react,
}: {
  to: string;
  subject: string;
  react: React.ReactElement;
}) {
  const { data, error } = await resend.emails.send({
    from: "YourApp <hello@yourapp.com>",
    to,
    subject,
    react,
  });

  if (error) {
    console.error("Email send failed:", error);
    throw new Error(error.message);
  }

  return data;
}
Set your from address once in a shared helper. Every template inherits it. When you rebrand or change domains, you update one file instead of six.

Building Each Template

1

Welcome email

The welcome email is your first impression. It needs your brand logo, a clear next-action CTA, and a layout that renders correctly across Gmail, Outlook, and Apple Mail. Sounds simple until you discover Outlook ignores max-width, Gmail strips <style> blocks, and Yahoo wraps your single-column layout into two columns on mobile.

emails/welcome.tsx
import {
  Html, Head, Body, Container, Section,
  Text, Button, Img, Preview,
} from "@react-email/components";

interface WelcomeEmailProps {
  name: string;
  loginUrl: string;
}

export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to YourApp, {name}</Preview>
      <Body style={{ backgroundColor: "#f9fafb", fontFamily: "sans-serif" }}>
        <Container style={{ maxWidth: "560px", margin: "0 auto", padding: "40px 20px" }}>
          <Img
            src="https://yourapp.com/logo.png"
            width={120}
            height={40}
            alt="YourApp"
          />
          <Section style={{ marginTop: "32px" }}>
            <Text style={{ fontSize: "24px", fontWeight: "bold" }}>
              Welcome, {name}
            </Text>
            <Text style={{ color: "#6b7280", lineHeight: "1.6" }}>
              Your account is ready. Here is what to do next:
            </Text>
            <Button
              href={loginUrl}
              style={{
                backgroundColor: "#000",
                color: "#fff",
                padding: "12px 24px",
                borderRadius: "6px",
                fontSize: "14px",
                textDecoration: "none",
              }}
            >
              Open your dashboard
            </Button>
          </Section>
        </Container>
      </Body>
    </Html>
  );
}

This works in a demo. In production, you will spend the next 4-6 hours handling: dark mode color inversions, Outlook conditional comments for the button (mso- padding hacks), a text-only fallback, proper <!DOCTYPE> declarations, and responsive font sizes for mobile. A production-ready welcome template ships with all of these solved and tested across 90+ email clients.

2

Email verification

Verification emails have a unique constraint: the token link must work exactly once, expire after a set window (typically 24 hours), and the email must reach the inbox fast enough that the user does not abandon the flow. If this email lands in spam, your signup funnel is broken.

emails/verify-email.tsx
import {
  Html, Head, Body, Container, Section,
  Text, Button, Preview, Hr,
} from "@react-email/components";

interface VerifyEmailProps {
  verifyUrl: string;
  expiresIn: string;
}

export function VerifyEmail({ verifyUrl, expiresIn }: VerifyEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Verify your email address</Preview>
      <Body style={{ backgroundColor: "#f9fafb", fontFamily: "sans-serif" }}>
        <Container style={{ maxWidth: "560px", margin: "0 auto", padding: "40px 20px" }}>
          <Text style={{ fontSize: "20px", fontWeight: "bold" }}>
            Confirm your email
          </Text>
          <Text style={{ color: "#6b7280", lineHeight: "1.6" }}>
            Click the button below to verify your email address.
            This link expires in {expiresIn}.
          </Text>
          <Button
            href={verifyUrl}
            style={{
              backgroundColor: "#000",
              color: "#fff",
              padding: "12px 24px",
              borderRadius: "6px",
            }}
          >
            Verify email
          </Button>
          <Hr style={{ margin: "32px 0", borderColor: "#e5e7eb" }} />
          <Text style={{ fontSize: "12px", color: "#9ca3af" }}>
            If you did not create an account, ignore this email.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

The hidden complexity: the raw URL must be visible as plain text below the button (some corporate email clients block buttons entirely), the "ignore this email" disclaimer is legally required in many jurisdictions, and the expire timestamp should adjust to the recipient's timezone. The email verification template handles all of this, including a fallback link, proper security copy, and accessible color contrast ratios.

3

Password reset

Password reset emails carry more weight than any other transactional email. They are a security boundary. If the email looks even slightly off - wrong logo, broken layout, suspicious link formatting - users assume it is phishing and file a support ticket instead of clicking.

emails/password-reset.tsx
import {
  Html, Head, Body, Container, Section,
  Text, Button, Preview, Hr,
} from "@react-email/components";

interface PasswordResetProps {
  resetUrl: string;
  ipAddress: string;
  userAgent: string;
}

export function PasswordResetEmail({
  resetUrl,
  ipAddress,
  userAgent,
}: PasswordResetProps) {
  return (
    <Html>
      <Head />
      <Preview>Reset your password</Preview>
      <Body style={{ backgroundColor: "#f9fafb", fontFamily: "sans-serif" }}>
        <Container style={{ maxWidth: "560px", margin: "0 auto", padding: "40px 20px" }}>
          <Text style={{ fontSize: "20px", fontWeight: "bold" }}>
            Password reset request
          </Text>
          <Text style={{ color: "#6b7280", lineHeight: "1.6" }}>
            Someone requested a password reset for your account.
            If this was you, click the button below.
          </Text>
          <Button
            href={resetUrl}
            style={{
              backgroundColor: "#000",
              color: "#fff",
              padding: "12px 24px",
              borderRadius: "6px",
            }}
          >
            Reset password
          </Button>
          <Hr style={{ margin: "32px 0", borderColor: "#e5e7eb" }} />
          <Text style={{ fontSize: "12px", color: "#9ca3af" }}>
            Request from {ipAddress} using {userAgent}.
            If you did not request this, you can safely ignore this email.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

Password reset emails need security metadata (IP, user agent, timestamp), a plaintext URL fallback, an expiration warning, and rock-solid rendering so the user trusts the email enough to click. Building this properly takes 6-8 hours. The password reset template includes security context blocks, branded header, accessible button styles, and dark mode support.

Never include the actual new password or the reset token as plain text in the email body. The reset URL should contain a one-time token that expires in 15-60 minutes.
4

Trial ending reminder

Trial ending emails are conversion emails disguised as notifications. The goal is not just to inform - it is to convert the user to a paid plan before the trial expires. You need two: one at 3 days out (nudge) and one at 1 day out (urgency).

emails/trial-ending.tsx
import {
  Html, Head, Body, Container, Section,
  Text, Button, Preview,
} from "@react-email/components";

interface TrialEndingProps {
  name: string;
  daysLeft: number;
  upgradeUrl: string;
  planName: string;
  price: string;
}

export function TrialEndingEmail({
  name,
  daysLeft,
  upgradeUrl,
  planName,
  price,
}: TrialEndingProps) {
  return (
    <Html>
      <Head />
      <Preview>
        Your trial ends in {daysLeft} {daysLeft === 1 ? "day" : "days"}
      </Preview>
      <Body style={{ backgroundColor: "#f9fafb", fontFamily: "sans-serif" }}>
        <Container style={{ maxWidth: "560px", margin: "0 auto", padding: "40px 20px" }}>
          <Text style={{ fontSize: "20px", fontWeight: "bold" }}>
            {daysLeft === 1
              ? "Your trial ends tomorrow"
              : `Your trial ends in ${daysLeft} days`}
          </Text>
          <Text style={{ color: "#6b7280", lineHeight: "1.6" }}>
            Hi {name}, your free trial is wrapping up.
            Upgrade to {planName} ({price}/mo) to keep everything
            you have built.
          </Text>
          <Button
            href={upgradeUrl}
            style={{
              backgroundColor: "#000",
              color: "#fff",
              padding: "12px 24px",
              borderRadius: "6px",
            }}
          >
            Upgrade now
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

The DIY version above is a notification. A conversion-optimized version includes a feature usage summary (showing what the user would lose), social proof, a pricing comparison, and urgency indicators that change based on daysLeft. Building those conditional layouts and testing them across clients takes a full day. The trial ending template includes all of these conversion elements, tested and ready to customize with your brand and pricing.

5

Payment receipt

Payment receipts are legally required in many regions and customers expect them instantly. The template needs to handle line items, tax calculations, currency formatting, billing address display, and a link to the invoice PDF.

emails/payment-receipt.tsx
import {
  Html, Head, Body, Container, Section,
  Text, Row, Column, Preview, Hr,
} from "@react-email/components";

interface LineItem {
  description: string;
  amount: string;
}

interface PaymentReceiptProps {
  customerName: string;
  invoiceNumber: string;
  date: string;
  lineItems: LineItem[];
  total: string;
  invoiceUrl: string;
}

export function PaymentReceiptEmail({
  customerName,
  invoiceNumber,
  lineItems,
  total,
  invoiceUrl,
}: PaymentReceiptProps) {
  return (
    <Html>
      <Head />
      <Preview>Payment receipt #{invoiceNumber}</Preview>
      <Body style={{ backgroundColor: "#f9fafb", fontFamily: "sans-serif" }}>
        <Container style={{ maxWidth: "560px", margin: "0 auto", padding: "40px 20px" }}>
          <Text style={{ fontSize: "20px", fontWeight: "bold" }}>
            Payment received
          </Text>
          <Text style={{ color: "#6b7280" }}>
            Thanks, {customerName}. Here is your receipt.
          </Text>
          <Section style={{
            border: "1px solid #e5e7eb",
            borderRadius: "8px",
            padding: "16px",
            marginTop: "24px",
          }}>
            {lineItems.map((item, i) => (
              <Row key={i}>
                <Column>
                  <Text style={{ margin: "4px 0" }}>{item.description}</Text>
                </Column>
                <Column align="right">
                  <Text style={{ margin: "4px 0" }}>{item.amount}</Text>
                </Column>
              </Row>
            ))}
            <Hr style={{ borderColor: "#e5e7eb" }} />
            <Row>
              <Column>
                <Text style={{ fontWeight: "bold" }}>Total</Text>
              </Column>
              <Column align="right">
                <Text style={{ fontWeight: "bold" }}>{total}</Text>
              </Column>
            </Row>
          </Section>
        </Container>
      </Body>
    </Html>
  );
}

The table layout above will break in Outlook, which does not support CSS border-radius or flexbox. Production receipts need <table>-based layouts with mso- conditional comments, proper currency localization, and a PDF download link. The invoice email template uses battle-tested table layouts that render identically in Outlook 2016 through Outlook 365.

6

Failed payment recovery

Failed payment emails recover revenue. The average SaaS loses 9% of MRR to involuntary churn from failed charges. A good dunning email needs a direct "update payment method" CTA, context on what the user will lose, and a tone that is helpful, not threatening.

emails/failed-payment.tsx
import {
  Html, Head, Body, Container,
  Text, Button, Preview,
} from "@react-email/components";

interface FailedPaymentProps {
  name: string;
  amount: string;
  updateUrl: string;
  retryDate: string;
}

export function FailedPaymentEmail({
  name,
  amount,
  updateUrl,
  retryDate,
}: FailedPaymentProps) {
  return (
    <Html>
      <Head />
      <Preview>Action required: payment of {amount} failed</Preview>
      <Body style={{ backgroundColor: "#f9fafb", fontFamily: "sans-serif" }}>
        <Container style={{ maxWidth: "560px", margin: "0 auto", padding: "40px 20px" }}>
          <Text style={{ fontSize: "20px", fontWeight: "bold" }}>
            Your payment did not go through
          </Text>
          <Text style={{ color: "#6b7280", lineHeight: "1.6" }}>
            Hi {name}, we could not process your payment of {amount}.
            Please update your payment method before {retryDate} to
            avoid any interruption to your account.
          </Text>
          <Button
            href={updateUrl}
            style={{
              backgroundColor: "#000",
              color: "#fff",
              padding: "12px 24px",
              borderRadius: "6px",
            }}
          >
            Update payment method
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

Effective dunning emails escalate over a sequence: gentle reminder on day 1, feature-loss warning on day 3, final notice on day 7. Each email in the sequence needs different copy, different urgency, and different CTAs. Building and testing a 3-email dunning sequence from scratch takes 1-2 days. The failed payment template includes the complete recovery sequence with escalating urgency and Stripe Billing Portal integration.


The Real Time Cost

Build vs. buy: the math

Each template above takes 4-8 hours to build, test across email clients, handle dark mode, fix Outlook rendering, add mobile responsiveness, and write accessible markup. Six templates at 6 hours each is 36 hours of email work - nearly a full work week spent on emails instead of your product.

With production-ready templates, the same setup takes an afternoon. You customize colors, drop in your logo, wire up the send triggers, and ship. The rendering bugs, client-specific hacks, and dark mode edge cases are already solved.


Wiring Templates to Your App

Once you have your templates (built or bought), the integration pattern is the same for all six. Here is a complete Next.js API route that handles the welcome email:

app/api/auth/signup/route.ts
import { sendEmail } from "@/lib/email";
import { WelcomeEmail } from "@/emails/welcome";
import { db } from "@/lib/db";

export async function POST(req: Request) {
  const { email, name, password } = await req.json();

  // Create user in your database
  const user = await db.user.create({
    data: { email, name, password: await hash(password) },
  });

  // Send welcome email - non-blocking
  sendEmail({
    to: email,
    subject: `Welcome to YourApp, ${name}`,
    react: <WelcomeEmail name={name} loginUrl="https://yourapp.com/login" />,
  }).catch((err) => {
    // Log but don't block signup if email fails
    console.error("Welcome email failed:", err);
  });

  return Response.json({ userId: user.id });
}
Fire email sends without await so the API response is not blocked by email delivery. Log failures separately. Users should never see a signup error because the email provider had a timeout.

Key takeaway
Six templates, one shared send helper, and a non-blocking integration pattern. Set up Resend with a single from address, build (or buy) your welcome, verification, password reset, trial ending, receipt, and failed payment templates, then wire each one as a fire-and-forget call in your API routes. Test every template in Outlook, Gmail, Apple Mail, and at least one mobile client. Handle dark mode with forced color inversion testing, not just prefers-color-scheme. The difference between building from scratch and using production-ready templates is about 36 hours of rendering bugs, client-specific hacks, and edge cases you discover one at a time.
R

React Emails Pro

Team

Building production-ready email templates with React Email. Writing about transactional email best practices, deliverability, and developer tooling.

Production-ready templates

Pick from 9 template packs built with React Email. One-time purchase, lifetime updates, tested across every major email client.

Browse all templates