Resend integration

Transactional email via Resend. Two template functions — verification OTP and password reset OTP. Inline CSS, spam-filter safe.

Setup

  1. Create a free account at resend.com
  2. Add and verify your sending domain in the Resend dashboard
  3. Create an API key: API Keys → Create API Key
  4. Set RESEND_FROM_EMAIL to a verified address on your domain (e.g. no-reply@yourdomain.com)
  5. Add RESEND_API_KEY and RESEND_FROM_EMAIL to .env.local

Email functions

lib/email.ts
import { Resend } from 'resend'

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

export async function sendVerificationEmail({
  to, name, otp
}: { to: string; name: string; otp: string }): Promise<ActionState<void>> {
  try {
    await resend.emails.send({
      from: process.env.RESEND_FROM_EMAIL!,
      to,
      subject: `Verify your email — ${process.env.NEXT_PUBLIC_APP_NAME}`,
      html: buildVerificationTemplate(name, otp),
      text: `Your verification code is: ${otp}. It expires in 10 minutes.`,
    })
    return { success: true }
  } catch {
    return {
      success: false,
      error: 'Failed to send email. Please try again.',
    }
  }
}

sendPasswordResetEmail follows the same pattern with a different subject line and email body copy.

Email templates

Templates use inline CSS only — external stylesheets are stripped by most email clients. The OTP code is rendered in a styled box:

  • Background: #f3f4f6
  • Border-radius: 8px
  • Font-size: 32px, font-family: monospace
  • Letter-spacing: 8px
  • Centered with padding

Each email includes:

  • A plain-text fallback (the text field in Resend's options)
  • Max-width: 600px centered layout
  • No images (improves deliverability)
  • Clear expiry notice (10 minutes)
  • Note for password reset: 'If you didn't request this, ignore this email'

Error handling

sendVerificationEmail and sendPasswordResetEmail both return ActionState<void>. On Resend API errors, they return:

{ success: false, error: 'Failed to send email. Please try again.' }

Callers (server actions) propagate this ActionState back to the client component, which displays it as a toast error.

Customising templates

To change email content, edit the buildVerificationTemplate and buildPasswordResetTemplate functions in lib/email.ts. Both accept name and otp as parameters.

To change the sender name that appears in the recipient's inbox, update the from field:

from: `${process.env.NEXT_PUBLIC_APP_NAME} <${process.env.RESEND_FROM_EMAIL}>`
// e.g. "MyApp <no-reply@myapp.com>"