Email Resend Skill
This skill provides workflows, best practices, and code patterns for sending transactional emails using Resend.
Overview
Resend is a modern email API designed for developers. It provides:
- Simple REST API and SDKs
- React Email integration for beautiful templates
- Built-in analytics and deliverability monitoring
- MCP server for AI-assisted email workflows
Prerequisites
1. Resend Account Setup
- Sign up at resend.com
- Generate an API key in the dashboard
- Add to your environment:
# .env.local
RESEND_API_KEY=re_xxxxx
RESEND_FROM_EMAIL="App Name <noreply@yourdomain.com>"
2. Domain Verification
Before sending from your domain, you must verify it:
- Go to Resend Dashboard → Domains → Add Domain
- Add these DNS records at your provider:
| Type | Name | Value | Purpose |
|---|---|---|---|
| TXT | @ or domain | v=spf1 include:_spf.resend.com ~all | SPF |
| CNAME | resend._domainkey | Provided by Resend | DKIM |
| TXT | _dmarc | v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com | DMARC |
- Wait for verification (usually 5-60 minutes)
3. MCP Server Setup (Optional - for Cursor AI)
To use Resend directly from Cursor's AI:
- Clone and build the MCP server:
cd ~/Desktop/Code
git clone https://github.com/resend/mcp-send-email.git mcp-send-email
cd mcp-send-email && npm install && npm run build
- Get the absolute path (this is critical):
realpath ~/Desktop/Code/mcp-send-email/build/index.js
# Output: /Users/YOUR_USERNAME/Desktop/Code/mcp-send-email/build/index.js
- Add to your
.cursor/mcp.json:
{
"mcpServers": {
"resend": {
"type": "command",
"command": "node /Users/YOUR_USERNAME/Desktop/Code/mcp-send-email/build/index.js --key=re_xxxxx --sender=noreply@yourdomain.com"
}
}
}
Common Gotcha: The path must be absolute and correct.
~/Code/vs~/Desktop/Code/will causeMODULE_NOT_FOUNDerrors. Always verify withrealpath.
Code Patterns
Basic Resend Client (TypeScript)
// lib/email/index.ts
import { Resend } from 'resend'
const resend = new Resend(process.env.RESEND_API_KEY)
const FROM_EMAIL = process.env.RESEND_FROM_EMAIL || 'App <noreply@example.com>'
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
export interface SendEmailOptions {
to: string
subject: string
html: string
text?: string
replyTo?: string
}
export async function sendEmail(options: SendEmailOptions) {
const { data, error } = await resend.emails.send({
from: FROM_EMAIL,
to: options.to,
subject: options.subject,
html: options.html,
text: options.text,
replyTo: options.replyTo,
})
if (error) {
console.error('[Email] Failed to send:', error)
throw new Error(`Failed to send email: ${error.message}`)
}
return data
}
Verification Email
export async function sendVerificationEmail(email: string, token: string) {
const verifyUrl = `${APP_URL}/verify-email?token=${token}`
return sendEmail({
to: email,
subject: 'Verify your email address',
html: `
<h2>Welcome!</h2>
<p>Please verify your email address by clicking the button below:</p>
<a href="${verifyUrl}" style="
display: inline-block;
background: #000;
color: #fff;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
margin: 16px 0;
">Verify Email</a>
<p>Or copy this link: ${verifyUrl}</p>
<p>This link expires in 24 hours.</p>
<p style="color: #666; font-size: 12px;">
If you didn't create an account, you can ignore this email.
</p>
`,
text: `Verify your email: ${verifyUrl}`,
})
}
Password Reset Email
export async function sendPasswordResetEmail(email: string, token: string) {
const resetUrl = `${APP_URL}/reset-password?token=${token}`
return sendEmail({
to: email,
subject: 'Reset your password',
html: `
<h2>Password Reset Request</h2>
<p>We received a request to reset your password. Click below to choose a new one:</p>
<a href="${resetUrl}" style="
display: inline-block;
background: #000;
color: #fff;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
margin: 16px 0;
">Reset Password</a>
<p>Or copy this link: ${resetUrl}</p>
<p>This link expires in 1 hour.</p>
<p style="color: #666; font-size: 12px;">
If you didn't request this, you can safely ignore this email.
</p>
`,
text: `Reset your password: ${resetUrl}`,
})
}
Welcome Email (after verification)
export async function sendWelcomeEmail(email: string, name?: string) {
return sendEmail({
to: email,
subject: 'Welcome to App Name!',
html: `
<h2>Welcome${name ? `, ${name}` : ''}!</h2>
<p>Your email has been verified and your account is ready.</p>
<p>Here are some things you can do:</p>
<ul>
<li>Complete your profile</li>
<li>Explore features</li>
<li>Check out the documentation</li>
</ul>
<a href="${APP_URL}" style="
display: inline-block;
background: #000;
color: #fff;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
margin: 16px 0;
">Get Started</a>
`,
text: `Welcome! Your account is ready. Get started at ${APP_URL}`,
})
}
Database Schema
Add these tables for email verification and password reset:
-- Add to users table
ALTER TABLE users ADD COLUMN email_verified_at TEXT;
-- Email verification tokens (one-time use)
CREATE TABLE email_verification_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE, -- SHA-256 hash, never store raw
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Password reset tokens (one-time use)
CREATE TABLE password_reset_tokens (
id TEXT PRIMARY KEY,
email TEXT NOT NULL, -- Use email, not user_id (user might not exist)
token_hash TEXT NOT NULL UNIQUE,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_verification_tokens_hash ON email_verification_tokens(token_hash);
CREATE INDEX idx_reset_tokens_hash ON password_reset_tokens(token_hash);
Token Generation Pattern
Use secure, URL-safe tokens for verification and reset links:
// lib/auth/tokens.ts
import { sha256 } from 'oslo/crypto'
import { encodeBase64url, encodeHex } from 'oslo/encoding'
// Token valid for 24 hours
const VERIFICATION_TOKEN_EXPIRY = 24 * 60 * 60 * 1000
// Reset token valid for 1 hour
const RESET_TOKEN_EXPIRY = 60 * 60 * 1000
/**
* Generate a cryptographically secure token
*/
export function generateToken(): string {
const bytes = new Uint8Array(32)
crypto.getRandomValues(bytes)
return encodeBase64url(bytes)
}
/**
* Hash a token for database storage
* Never store raw tokens - always hash them
*/
export async function hashToken(token: string): Promise<string> {
return encodeHex(await sha256(new TextEncoder().encode(token)))
}
Token Security Rules
- Never store raw tokens - Always hash with SHA-256 before storing
- One-time use - Delete token after successful validation
- Delete existing tokens - When creating new token, delete any existing ones for that user/email
- Short expiry for reset - Password reset tokens should expire in 1 hour max
- Longer expiry for verification - Email verification can be 24-48 hours
Rate Limiting
Prevent email abuse with rate limiting:
// Per-email rate limits
const EMAIL_RATE_LIMITS = {
verification: { max: 3, window: 60 * 60 * 1000 }, // 3 per hour
passwordReset: { max: 3, window: 60 * 60 * 1000 }, // 3 per hour
general: { max: 10, window: 24 * 60 * 60 * 1000 }, // 10 per day
}
Resend API Limits
| Plan | Emails/day | Emails/month | Rate limit |
|---|---|---|---|
| Free | 100 | 3,000 | 2/second |
| Pro | 5,000+ | Based on plan | 10/second |
OAuth vs Password Users
When implementing email verification, handle OAuth and password users differently:
// OAuth users (Google, Apple, etc.) - pre-verified
const user = createUser({
// ...
email_verified_at: new Date().toISOString(), // Trust OAuth provider
})
// Password users - require verification
const user = createUser({
// ...
email_verified_at: null, // Must verify via email
})
Verification Flow by Auth Type
| Auth Type | Email Verified? | Verification Required? |
|---|---|---|
| Google OAuth | Yes (by Google) | No |
| Apple OAuth | Yes (by Apple) | No |
| Email/Password | No | Yes - block until verified |
| Magic Link | Yes (implicit) | No |
Handling Unverified Login Attempts
// In login API
if (!user.email_verified_at) {
return NextResponse.json({
error: 'Please verify your email before signing in.',
code: 'EMAIL_NOT_VERIFIED',
requiresVerification: true,
}, { status: 403 })
}
Re-signup for Unverified Users
If a user tries to sign up with an email that exists but isn't verified, resend the verification:
const existingUser = getUserByEmail(email)
if (existingUser && !existingUser.email_verified_at) {
// Resend verification instead of returning error
const { token } = await createVerificationToken(existingUser.id)
await sendVerificationEmail(email, token)
return { requiresVerification: true }
}
Next.js Integration
Next.js 16+ (proxy.ts)
Breaking Change: Next.js 16 replaced
middleware.tswithproxy.ts
For protected routes, use proxy.ts for fast redirects only:
// src/proxy.ts
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
// Allow auth routes
if (['/login', '/verify-email', '/forgot-password', '/reset-password']
.some(r => pathname.startsWith(r))) {
return NextResponse.next()
}
// Let API routes handle their own auth
return NextResponse.next()
}
Important: Don't do heavy auth validation in proxy. Check email_verified_at in your API routes and server components instead.
Next.js 16 and earlier
Use middleware.ts with the same logic, but export as middleware instead of proxy.
Best Practices
Subject Lines
- Keep under 60 characters
- Be specific and action-oriented
- Avoid spam triggers (see references/deliverability.md)
Email Copy
- Front-load important information
- Use clear CTAs
- Always include plain text fallback
- Keep emails focused on one purpose
Transactional vs Marketing
- Transactional: Triggered by user action (verification, reset, receipts)
- Marketing: Promotional content (newsletters, announcements)
- Keep them separate - different sending reputations
Error Handling
- Log all email failures
- Have fallback mechanisms (show token in UI for dev)
- Don't block user actions on email failures
Verification Strategies
| Strategy | UX | Security | Use Case |
|---|---|---|---|
| Block until verified | Friction | High | Financial, healthcare |
| Soft verification (banner) | Smooth | Medium | Social, content apps |
| No verification | Seamless | Low | Low-risk apps |
Implementation Checklist
Email Verification Flow
- Add
email_verified_atcolumn to users table - Create
email_verification_tokenstable - Install
resendandoslopackages - Create email service (
lib/email/index.ts) - Create token utilities (
lib/auth/tokens.ts) - Modify signup to send verification email
- Create
/api/auth/verify-emailendpoint - Create
/api/auth/resend-verificationendpoint (rate limited) - Create
/verify-emailpage with resend UI - Update login to check
email_verified_at - Handle OAuth users as pre-verified
Password Reset Flow
- Create
password_reset_tokenstable - Create
/api/auth/forgot-passwordendpoint - Create
/api/auth/reset-passwordendpoint - Create
/forgot-passwordpage - Create
/reset-passwordpage - Add "Forgot password?" link to login page
Environment Variables
RESEND_API_KEY=re_xxxxx
RESEND_FROM_EMAIL="App Name <noreply@yourdomain.com>"
NEXT_PUBLIC_APP_URL=https://yourapp.com
References
- Deliverability Guide - DNS, spam prevention, reputation
- Email Templates - Copy best practices, compliance
- React Email Patterns - Component-based email templates
Common Issues & Troubleshooting
MCP Server "MODULE_NOT_FOUND"
Cause: Wrong path in .cursor/mcp.json
Fix: Use absolute path, verify with realpath:
realpath ~/Desktop/Code/mcp-send-email/build/index.js
Emails Going to Spam
- Verify DNS records (SPF, DKIM, DMARC) are correct
- Check sender domain matches authenticated domain
- Review email content for spam trigger words
- Test with mail-tester.com
Token Validation Failing
- Ensure you're hashing the token before lookup
- Check token hasn't expired
- Verify token wasn't already consumed (one-time use)
- Check for URL encoding issues in the token
OAuth Users Can't Reset Password
OAuth-only users don't have passwords. Check for password_hash:
if (!user.password_hash) {
return { error: 'This account uses social login.' }
}
Next.js 16 Proxy Not Working
- File must be
src/proxy.ts(notmiddleware.ts) - Export must be
proxy(notmiddleware) - Proxy runs on Node.js runtime, not Edge
Dependencies
Install the Resend SDK:
npm install resend
For token hashing (recommended):
npm install oslo
For React Email templates (optional):
npm install @react-email/components react-email