Resend is the cleanest way to send transactional emails from Next.js — type-safe, built for developers, and free for the first 3,000 emails per month. Here's how to install it, set up domain verification, write the three core transactional emails every SaaS needs, and trigger them from Stripe webhooks — using the same pattern deployed in GetLaunchpad.
Why Resend over SendGrid or Mailgun
The legacy email providers were designed for marketing teams, not developers. Resend was built API-first with TypeScript types, a clean REST API, and first-class support for React Email templates. You write email markup in the same TSX you use everywhere else, and the SDK enforces the shape of every call at compile time.
Install the package
npm install resend
Grab your API key from the Resend dashboard and add it to your environment:
RESEND_API_KEY=re_your_key_here
Create the client singleton
// lib/resend.ts
import "server-only";
import { Resend } from "resend";
export const resend = new Resend(process.env.RESEND_API_KEY);
The server-only import is essential — it causes a build error if this file is accidentally imported into a client component, keeping your API key out of the browser bundle. Always create the Resend instance once and export it rather than calling new Resend() in every route handler.
HTML templates vs React Email
You have two options for authoring email templates:
- Plain HTML strings. Pass a
html string to resend.emails.send(). Fast to write, but you lose type safety and reusable components. Fine for simple one-off emails. - React Email components. Install
@react-email/components and write emails as React components. You get TypeScript, reusable layout components, and live preview in a local dev server. This is the recommended approach for anything beyond a single template.
For the three templates below, plain HTML strings keep the example self-contained. Swap in React Email components when your template library grows.
Domain verification
Before sending from your own domain (hello@yourdomain.com instead of a Resend subdomain), you must add DNS records. In the Resend dashboard, go to Domains → Add Domain and enter your domain. Resend will give you three records to add:
- SPF — a
TXT record that tells receiving servers Resend is authorized to send on your behalf. - DKIM — a
CNAME record that adds a cryptographic signature to outgoing mail, preventing spoofing. - DMARC — an optional but recommended
TXT record that instructs receiving servers what to do with mail that fails SPF or DKIM checks.
DNS propagation typically takes a few minutes on modern providers like Cloudflare or Vercel DNS. Resend shows a green checkmark once all records verify. You cannot send from a custom domain until verification passes.
The three core transactional emails for SaaS
1. Welcome email on signup
Send this when a new user creates an account. The best trigger point is your POST /api/user upsert route — the same place you write the new user to Supabase.
// Inside POST /api/user, after the Supabase upsert:
await resend.emails.send({
from: "GetLaunchpad <hello@getlaunchpad.net>",
to: userEmail,
subject: "Welcome to GetLaunchpad",
html: `
<p>Hi ${firstName},</p>
<p>Thanks for signing up. Your account is ready.</p>
<p><a href="https://getlaunchpad.net/dashboard">Open your dashboard</a></p>
`,
});
2. Receipt on payment
Send this when a checkout.session.completed or invoice.payment_succeeded webhook fires. Pull the amount and next billing date from the Stripe event object.
// Inside your Stripe webhook handler, after upserting the subscription:
if (event.type === "checkout.session.completed") {
const session = event.data.object;
await resend.emails.send({
from: "GetLaunchpad <hello@getlaunchpad.net>",
to: session.customer_email!,
subject: "Your payment was successful",
html: `
<p>Thanks for subscribing to GetLaunchpad Pro.</p>
<p>Amount charged: ${(session.amount_total! / 100).toFixed(2)} USD</p>
<p>Your subscription is now active.</p>
`,
});
}
3. Cancellation notice on subscription end
Send this when a customer.subscription.deleted event arrives. Keep the tone neutral — give the user a way to resubscribe.
if (event.type === "customer.subscription.deleted") {
const subscription = event.data.object;
// Look up the user email from your Supabase subscriptions table
await resend.emails.send({
from: "GetLaunchpad <hello@getlaunchpad.net>",
to: userEmail,
subject: "Your GetLaunchpad subscription has ended",
html: `
<p>Your Pro subscription has been cancelled.</p>
<p>You can resubscribe at any time from your dashboard.</p>
<p><a href="https://getlaunchpad.net/dashboard">Resubscribe</a></p>
`,
});
}
Triggering from Stripe webhooks vs user actions
There are two trigger points for transactional emails:
- Stripe webhooksare the right place for payment-related emails (receipts, cancellations, renewals, failed payments). Webhooks are the only reliable signal — do not send a receipt from your checkout route because Stripe's payment confirmation happens asynchronously after the session is created.
- User actions are the right place for account-related emails (welcome on signup, password reset, invitation accepted). These are triggered by your own route handlers where you control the exact moment the action completes.
Never send a transactional email from both a webhook and a user action for the same event — the user will receive duplicates. Pick one source of truth per email type.
Testing with a real send
Resend has no sandbox mode — every call sends a real email. For local development, use a personal email address or a service like Mailtrap as the recipient. A minimal smoke test:
// One-off test script: npx tsx scripts/test-email.ts
import { resend } from "../lib/resend";
const { data, error } = await resend.emails.send({
from: "onboarding@resend.dev", // Resend's shared domain — no DNS setup needed
to: "you@example.com",
subject: "Test email",
html: "<p>Hello from Resend</p>",
});
console.log({ data, error });
Using onboarding@resend.dev as the sender lets you test without configuring DNS records first. Switch to your verified domain before going to production.
Monitoring delivery in the Resend dashboard
Every email sent through Resend appears in the Emails tab of the dashboard with its delivery status: delivered, bounced, complained, or clicked (if click tracking is enabled). This is your primary tool for diagnosing delivery issues — if a user says they did not receive a welcome email, check here first.
Resend also exposes webhook events for delivery status changes, so you can write bounced addresses to your database and suppress future sends automatically.
The complete Resend integration — client singleton, welcome email, payment receipt, and cancellation notice — is pre-wired in GetLaunchpad, a Next.js 16 SaaS boilerplate. Every Stripe webhook already triggers the right email on day one.