DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Transactional Email in Next.js with Resend: Welcome Emails, Password Reset, and Stripe Receipts

The Email Problem Every SaaS Has

Users forget their password. They need to verify their email. A subscription renews. Someone invites a teammate. All of these require transactional email -- reliable, deliverable, templated.

Here's how to set up Resend in Next.js properly.

Setup

npm install resend react-email @react-email/components
Enter fullscreen mode Exit fullscreen mode
// lib/email.ts
import { Resend } from 'resend'

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

export const FROM_EMAIL = 'Atlas <hello@whoffagents.com>'
export const REPLY_TO = 'support@whoffagents.com'
Enter fullscreen mode Exit fullscreen mode

React Email Templates

// emails/WelcomeEmail.tsx
import {
  Html, Head, Body, Container, Heading, Text, Button, Hr
} from '@react-email/components'

interface WelcomeEmailProps {
  name: string
  dashboardUrl: string
}

export default function WelcomeEmail({ name, dashboardUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Body style={{ backgroundColor: '#0a0a0a', fontFamily: 'sans-serif' }}>
        <Container style={{ maxWidth: '600px', margin: '40px auto', padding: '40px' }}>
          <Heading style={{ color: '#ffffff', fontSize: '24px' }}>
            Welcome to whoffagents.com, {name}
          </Heading>
          <Text style={{ color: '#94a3b8', fontSize: '16px', lineHeight: '1.6' }}>
            Your account is ready. Here's what to do next:
          </Text>
          <Button
            href={dashboardUrl}
            style={{
              backgroundColor: '#0062b8',
              color: '#ffffff',
              padding: '12px 24px',
              borderRadius: '8px',
              textDecoration: 'none',
              display: 'inline-block',
              marginTop: '16px'
            }}
          >
            Go to Dashboard
          </Button>
          <Hr style={{ borderColor: '#1e293b', margin: '32px 0' }} />
          <Text style={{ color: '#475569', fontSize: '14px' }}>
            You received this because you signed up at whoffagents.com.
          </Text>
        </Container>
      </Body>
    </Html>
  )
}
Enter fullscreen mode Exit fullscreen mode

Sending the Email

// lib/send-email.ts
import { resend, FROM_EMAIL } from './email'
import WelcomeEmail from '@/emails/WelcomeEmail'

export async function sendWelcomeEmail(to: string, name: string) {
  const { data, error } = await resend.emails.send({
    from: FROM_EMAIL,
    to,
    subject: `Welcome to whoffagents.com, ${name}`,
    react: WelcomeEmail({ name, dashboardUrl: 'https://whoffagents.com/dashboard' })
  })

  if (error) {
    console.error('Failed to send welcome email:', error)
    throw new Error('Email send failed')
  }

  return data
}
Enter fullscreen mode Exit fullscreen mode

Password Reset Flow

// app/api/auth/forgot-password/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { resend, FROM_EMAIL } from '@/lib/email'
import crypto from 'crypto'

export async function POST(req: NextRequest) {
  const { email } = await req.json()

  const user = await db.user.findUnique({ where: { email } })

  // Always return success to prevent email enumeration
  if (!user) return NextResponse.json({ success: true })

  const token = crypto.randomBytes(32).toString('hex')
  const expires = new Date(Date.now() + 60 * 60 * 1000) // 1 hour

  await db.passwordResetToken.upsert({
    where: { userId: user.id },
    create: { userId: user.id, token, expires },
    update: { token, expires }
  })

  const resetUrl = `${process.env.NEXTAUTH_URL}/auth/reset-password?token=${token}`

  await resend.emails.send({
    from: FROM_EMAIL,
    to: email,
    subject: 'Reset your password',
    html: `<p>Click <a href="${resetUrl}">here</a> to reset your password. Link expires in 1 hour.</p>`
  })

  return NextResponse.json({ success: true })
}
Enter fullscreen mode Exit fullscreen mode

Stripe Subscription Emails

// Triggered from Stripe webhook
async function sendSubscriptionConfirmation(email: string, plan: string, amount: number) {
  await resend.emails.send({
    from: FROM_EMAIL,
    to: email,
    subject: 'Your subscription is active',
    html: [
      '<h2>Subscription confirmed</h2>',
      `<p>You're now on the <strong>${plan}</strong> plan.</p>`,
      `<p>Amount: $${(amount / 100).toFixed(2)}/month</p>`,
      '<p>Manage your billing anytime from your dashboard.</p>'
    ].join('')
  })
}
Enter fullscreen mode Exit fullscreen mode

Email Preview in Development

# Preview templates at http://localhost:3001
npx email dev --dir ./emails
Enter fullscreen mode Exit fullscreen mode

Pre-Built in the AI SaaS Starter Kit

Ships with Resend configured, welcome email, password reset flow, and Stripe receipt templates -- all with React Email components.

$99 one-time at whoffagents.com

Top comments (0)