1185 lines
29 KiB
Markdown
1185 lines
29 KiB
Markdown
---
|
|
description: Interactive Resend email setup wizard. Configures transactional and marketing emails, React Email templates, and environment variables for Cloudflare Workers.
|
|
---
|
|
|
|
# Email Setup Command
|
|
|
|
<command_purpose> Guide developers through complete Resend email integration with automated code generation, React Email templates, domain configuration, and MCP-driven email setup. </command_purpose>
|
|
|
|
## Introduction
|
|
|
|
<role>Senior Email Integration Engineer with expertise in Resend, React Email, and Cloudflare Workers email delivery</role>
|
|
|
|
**This command will**:
|
|
- Detect project type (Tanstack Start or API-only Worker)
|
|
- Install Resend SDK and React Email dependencies
|
|
- Generate React Email template components
|
|
- Create email server functions for Tanstack Start
|
|
- Configure transactional email handlers (verification, password reset)
|
|
- Generate marketing email examples (newsletter)
|
|
- Set up domain verification configuration
|
|
- Configure environment variables and secrets
|
|
- Add error handling and retry logic with D1
|
|
|
|
## Prerequisites
|
|
|
|
<requirements>
|
|
- Cloudflare Workers project (Tanstack Start or Hono)
|
|
- Resend account: https://resend.com (free tier available)
|
|
- API key from Resend dashboard
|
|
- D1 database configured for email retry tracking (optional)
|
|
</requirements>
|
|
|
|
## Main Tasks
|
|
|
|
### 1. Detect Project Type & Email Requirements
|
|
|
|
**Ask User**:
|
|
```markdown
|
|
📧 Email Setup Wizard
|
|
|
|
1. What project type are you using?
|
|
a) Tanstack Start (full-stack)
|
|
b) Standalone Worker (Hono/plain TS)
|
|
|
|
2. What email flows do you need?
|
|
a) Transactional only (verification, password reset)
|
|
b) Marketing only (newsletters, announcements)
|
|
c) Both transactional and marketing
|
|
d) Custom email patterns
|
|
```
|
|
|
|
**Decision Logic**:
|
|
```
|
|
If Tanstack Start + Any type:
|
|
→ Use createServerFn for email handlers
|
|
→ Generate React Email templates
|
|
→ Use @react-email/components
|
|
|
|
If Standalone Worker:
|
|
→ Use Worker handlers
|
|
→ Generate React Email templates
|
|
→ Use @react-email/components
|
|
```
|
|
|
|
### 2. Install Dependencies
|
|
|
|
**For Tanstack Start**:
|
|
```bash
|
|
npm install resend @react-email/components @react-email/render
|
|
npm install -D @types/react @types/react-dom
|
|
```
|
|
|
|
**For Standalone Worker**:
|
|
```bash
|
|
npm install resend @react-email/components @react-email/render
|
|
npm install -D @types/react @types/react-dom
|
|
```
|
|
|
|
**React Email Setup**:
|
|
```bash
|
|
# Add React Email CLI (optional, for preview server)
|
|
npm install -D react-email
|
|
```
|
|
|
|
### 3. Generate React Email Templates
|
|
|
|
#### Template: Email Verification
|
|
|
|
**Generate File**: `emails/verify-email.tsx`
|
|
|
|
```tsx
|
|
import {
|
|
Body,
|
|
Button,
|
|
Container,
|
|
Head,
|
|
Heading,
|
|
Html,
|
|
Link,
|
|
Preview,
|
|
Section,
|
|
Text,
|
|
} from '@react-email/components';
|
|
|
|
interface VerifyEmailProps {
|
|
verificationUrl: string;
|
|
email: string;
|
|
}
|
|
|
|
const baseUrl = process.env.RESEND_BASE_URL || 'https://example.com';
|
|
|
|
export function VerifyEmail({ verificationUrl, email }: VerifyEmailProps) {
|
|
return (
|
|
<Html>
|
|
<Head />
|
|
<Preview>Verify your email address</Preview>
|
|
<Body style={main}>
|
|
<Container style={container}>
|
|
<Section style={box}>
|
|
<Heading style={heading}>Verify your email</Heading>
|
|
<Text style={paragraph}>
|
|
Thanks for signing up! Please verify your email address to complete your registration.
|
|
</Text>
|
|
<Button style={button} href={verificationUrl}>
|
|
Verify Email
|
|
</Button>
|
|
<Text style={paragraph}>
|
|
Or copy and paste this link:
|
|
</Text>
|
|
<Link style={link} href={verificationUrl}>
|
|
{verificationUrl}
|
|
</Link>
|
|
<Text style={paragraph}>
|
|
This link expires in 24 hours. If you didn't create this account, you can safely ignore this email.
|
|
</Text>
|
|
</Section>
|
|
<Text style={footer}>
|
|
© 2025 Your Company. All rights reserved.
|
|
</Text>
|
|
</Container>
|
|
</Body>
|
|
</Html>
|
|
);
|
|
}
|
|
|
|
// Styles
|
|
const main = {
|
|
backgroundColor: '#f6f9fc',
|
|
fontFamily:
|
|
'-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen-Sans",Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
|
};
|
|
|
|
const container = {
|
|
backgroundColor: '#ffffff',
|
|
margin: '0 auto',
|
|
padding: '20px 0 48px',
|
|
marginBottom: '64px',
|
|
};
|
|
|
|
const box = {
|
|
padding: '0 48px',
|
|
};
|
|
|
|
const heading = {
|
|
color: '#1f2937',
|
|
fontSize: '24px',
|
|
fontWeight: 'bold',
|
|
margin: '16px 0',
|
|
};
|
|
|
|
const paragraph = {
|
|
color: '#525252',
|
|
fontSize: '16px',
|
|
lineHeight: '26px',
|
|
margin: '16px 0',
|
|
};
|
|
|
|
const button = {
|
|
backgroundColor: '#000000',
|
|
borderRadius: '4px',
|
|
color: '#fff',
|
|
fontSize: '16px',
|
|
fontWeight: 'bold',
|
|
textDecoration: 'none',
|
|
textAlign: 'center' as const,
|
|
display: 'block',
|
|
padding: '12px 20px',
|
|
margin: '16px 0',
|
|
};
|
|
|
|
const link = {
|
|
color: '#0000ee',
|
|
textDecoration: 'underline',
|
|
};
|
|
|
|
const footer = {
|
|
color: '#8898aa',
|
|
fontSize: '12px',
|
|
margin: '16px 0',
|
|
};
|
|
```
|
|
|
|
#### Template: Password Reset
|
|
|
|
**Generate File**: `emails/password-reset.tsx`
|
|
|
|
```tsx
|
|
import {
|
|
Body,
|
|
Button,
|
|
Container,
|
|
Head,
|
|
Heading,
|
|
Html,
|
|
Link,
|
|
Preview,
|
|
Section,
|
|
Text,
|
|
} from '@react-email/components';
|
|
|
|
interface PasswordResetProps {
|
|
resetUrl: string;
|
|
email: string;
|
|
}
|
|
|
|
export function PasswordReset({ resetUrl, email }: PasswordResetProps) {
|
|
return (
|
|
<Html>
|
|
<Head />
|
|
<Preview>Reset your password</Preview>
|
|
<Body style={main}>
|
|
<Container style={container}>
|
|
<Section style={box}>
|
|
<Heading style={heading}>Reset your password</Heading>
|
|
<Text style={paragraph}>
|
|
We received a request to reset your password. Click the button below to create a new password.
|
|
</Text>
|
|
<Button style={button} href={resetUrl}>
|
|
Reset Password
|
|
</Button>
|
|
<Text style={paragraph}>
|
|
Or copy and paste this link:
|
|
</Text>
|
|
<Link style={link} href={resetUrl}>
|
|
{resetUrl}
|
|
</Link>
|
|
<Text style={paragraph}>
|
|
This link expires in 1 hour. If you didn't request a password reset, you can safely ignore this email.
|
|
</Text>
|
|
<Text style={hint}>
|
|
For security, never share this link with anyone.
|
|
</Text>
|
|
</Section>
|
|
<Text style={footer}>
|
|
© 2025 Your Company. All rights reserved.
|
|
</Text>
|
|
</Container>
|
|
</Body>
|
|
</Html>
|
|
);
|
|
}
|
|
|
|
// Styles (same as VerifyEmail)
|
|
const main = {
|
|
backgroundColor: '#f6f9fc',
|
|
fontFamily:
|
|
'-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen-Sans",Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
|
};
|
|
|
|
const container = {
|
|
backgroundColor: '#ffffff',
|
|
margin: '0 auto',
|
|
padding: '20px 0 48px',
|
|
marginBottom: '64px',
|
|
};
|
|
|
|
const box = {
|
|
padding: '0 48px',
|
|
};
|
|
|
|
const heading = {
|
|
color: '#1f2937',
|
|
fontSize: '24px',
|
|
fontWeight: 'bold',
|
|
margin: '16px 0',
|
|
};
|
|
|
|
const paragraph = {
|
|
color: '#525252',
|
|
fontSize: '16px',
|
|
lineHeight: '26px',
|
|
margin: '16px 0',
|
|
};
|
|
|
|
const hint = {
|
|
color: '#f59e0b',
|
|
fontSize: '14px',
|
|
fontStyle: 'italic',
|
|
margin: '16px 0',
|
|
};
|
|
|
|
const button = {
|
|
backgroundColor: '#000000',
|
|
borderRadius: '4px',
|
|
color: '#fff',
|
|
fontSize: '16px',
|
|
fontWeight: 'bold',
|
|
textDecoration: 'none',
|
|
textAlign: 'center' as const,
|
|
display: 'block',
|
|
padding: '12px 20px',
|
|
margin: '16px 0',
|
|
};
|
|
|
|
const link = {
|
|
color: '#0000ee',
|
|
textDecoration: 'underline',
|
|
};
|
|
|
|
const footer = {
|
|
color: '#8898aa',
|
|
fontSize: '12px',
|
|
margin: '16px 0',
|
|
};
|
|
```
|
|
|
|
#### Template: Newsletter
|
|
|
|
**Generate File**: `emails/newsletter.tsx`
|
|
|
|
```tsx
|
|
import {
|
|
Body,
|
|
Button,
|
|
Container,
|
|
Head,
|
|
Heading,
|
|
Html,
|
|
Link,
|
|
Preview,
|
|
Section,
|
|
Text,
|
|
} from '@react-email/components';
|
|
|
|
interface NewsletterProps {
|
|
month: string;
|
|
articles: Array<{
|
|
title: string;
|
|
description: string;
|
|
url: string;
|
|
}>;
|
|
unsubscribeUrl: string;
|
|
}
|
|
|
|
export function Newsletter({ month, articles, unsubscribeUrl }: NewsletterProps) {
|
|
return (
|
|
<Html>
|
|
<Head />
|
|
<Preview>{month} Newsletter - Latest updates</Preview>
|
|
<Body style={main}>
|
|
<Container style={container}>
|
|
<Section style={header}>
|
|
<Heading style={heading}>{month} Newsletter</Heading>
|
|
<Text style={subtitle}>Your monthly roundup of updates and insights</Text>
|
|
</Section>
|
|
|
|
{articles.map((article, index) => (
|
|
<Section key={index} style={articleSection}>
|
|
<Heading style={articleHeading}>{article.title}</Heading>
|
|
<Text style={paragraph}>{article.description}</Text>
|
|
<Button style={button} href={article.url}>
|
|
Read More
|
|
</Button>
|
|
</Section>
|
|
))}
|
|
|
|
<Section style={footer}>
|
|
<Text style={footerText}>
|
|
© 2025 Your Company. All rights reserved.
|
|
</Text>
|
|
<Link style={unsubscribeLink} href={unsubscribeUrl}>
|
|
Unsubscribe
|
|
</Link>
|
|
</Section>
|
|
</Container>
|
|
</Body>
|
|
</Html>
|
|
);
|
|
}
|
|
|
|
// Styles
|
|
const main = {
|
|
backgroundColor: '#f6f9fc',
|
|
fontFamily:
|
|
'-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen-Sans",Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
|
};
|
|
|
|
const container = {
|
|
backgroundColor: '#ffffff',
|
|
margin: '0 auto',
|
|
padding: '48px 0',
|
|
marginBottom: '64px',
|
|
};
|
|
|
|
const header = {
|
|
backgroundColor: '#000000',
|
|
padding: '32px 48px',
|
|
};
|
|
|
|
const heading = {
|
|
color: '#ffffff',
|
|
fontSize: '32px',
|
|
fontWeight: 'bold',
|
|
margin: '0',
|
|
};
|
|
|
|
const subtitle = {
|
|
color: '#cccccc',
|
|
fontSize: '16px',
|
|
margin: '8px 0 0 0',
|
|
};
|
|
|
|
const articleSection = {
|
|
padding: '32px 48px',
|
|
borderBottom: '1px solid #e5e7eb',
|
|
};
|
|
|
|
const articleHeading = {
|
|
color: '#1f2937',
|
|
fontSize: '20px',
|
|
fontWeight: 'bold',
|
|
margin: '0 0 16px 0',
|
|
};
|
|
|
|
const paragraph = {
|
|
color: '#525252',
|
|
fontSize: '16px',
|
|
lineHeight: '26px',
|
|
margin: '16px 0',
|
|
};
|
|
|
|
const button = {
|
|
backgroundColor: '#000000',
|
|
borderRadius: '4px',
|
|
color: '#fff',
|
|
fontSize: '16px',
|
|
fontWeight: 'bold',
|
|
textDecoration: 'none',
|
|
textAlign: 'center' as const,
|
|
display: 'inline-block',
|
|
padding: '12px 20px',
|
|
margin: '16px 0',
|
|
};
|
|
|
|
const footer = {
|
|
padding: '32px 48px',
|
|
};
|
|
|
|
const footerText = {
|
|
color: '#8898aa',
|
|
fontSize: '12px',
|
|
margin: '0 0 16px 0',
|
|
};
|
|
|
|
const unsubscribeLink = {
|
|
color: '#8898aa',
|
|
fontSize: '12px',
|
|
textDecoration: 'underline',
|
|
};
|
|
```
|
|
|
|
### 4. Generate Email Server Functions (Tanstack Start)
|
|
|
|
**Generate File**: `server/emails/send-verify-email.ts`
|
|
|
|
```typescript
|
|
import { createServerFn } from '@tanstack/start';
|
|
import { Resend } from 'resend';
|
|
import { VerifyEmail } from '@/emails/verify-email';
|
|
|
|
interface SendVerifyEmailInput {
|
|
to: string;
|
|
verificationUrl: string;
|
|
}
|
|
|
|
export const sendVerifyEmail = createServerFn(
|
|
'POST',
|
|
async (input: SendVerifyEmailInput, context) => {
|
|
const { env } = context.cloudflare;
|
|
const resend = new Resend(env.RESEND_API_KEY);
|
|
|
|
try {
|
|
const { data, error } = await resend.emails.send({
|
|
from: 'noreply@yourdomain.com',
|
|
to: input.to,
|
|
subject: 'Verify your email address',
|
|
react: VerifyEmail({
|
|
verificationUrl: input.verificationUrl,
|
|
email: input.to,
|
|
}),
|
|
});
|
|
|
|
if (error) {
|
|
console.error('Resend error:', error);
|
|
throw new Error(`Failed to send verification email: ${error.message}`);
|
|
}
|
|
|
|
// Log sent email for audit trail
|
|
if (env.DB) {
|
|
await env.DB.prepare(
|
|
`INSERT INTO sent_emails (id, to, subject, type, email_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
).bind(
|
|
crypto.randomUUID(),
|
|
input.to,
|
|
'Verify your email address',
|
|
'verification',
|
|
data.id,
|
|
new Date().toISOString()
|
|
).run();
|
|
}
|
|
|
|
return { success: true, emailId: data.id };
|
|
} catch (error) {
|
|
console.error('Email send error:', error);
|
|
|
|
// Store failed email for retry
|
|
if (env.DB) {
|
|
await env.DB.prepare(
|
|
`INSERT INTO failed_emails (id, to, subject, type, error, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
).bind(
|
|
crypto.randomUUID(),
|
|
input.to,
|
|
'Verify your email address',
|
|
'verification',
|
|
error instanceof Error ? error.message : 'Unknown error',
|
|
new Date().toISOString()
|
|
).run();
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
error: 'Email delivery failed. Please try again later.',
|
|
};
|
|
}
|
|
}
|
|
);
|
|
```
|
|
|
|
**Generate File**: `server/emails/send-password-reset.ts`
|
|
|
|
```typescript
|
|
import { createServerFn } from '@tanstack/start';
|
|
import { Resend } from 'resend';
|
|
import { PasswordReset } from '@/emails/password-reset';
|
|
|
|
interface SendPasswordResetInput {
|
|
to: string;
|
|
resetUrl: string;
|
|
}
|
|
|
|
export const sendPasswordReset = createServerFn(
|
|
'POST',
|
|
async (input: SendPasswordResetInput, context) => {
|
|
const { env } = context.cloudflare;
|
|
const resend = new Resend(env.RESEND_API_KEY);
|
|
|
|
try {
|
|
const { data, error } = await resend.emails.send({
|
|
from: 'noreply@yourdomain.com',
|
|
to: input.to,
|
|
subject: 'Reset your password',
|
|
react: PasswordReset({
|
|
resetUrl: input.resetUrl,
|
|
email: input.to,
|
|
}),
|
|
});
|
|
|
|
if (error) {
|
|
console.error('Resend error:', error);
|
|
throw new Error(`Failed to send password reset email: ${error.message}`);
|
|
}
|
|
|
|
// Log sent email
|
|
if (env.DB) {
|
|
await env.DB.prepare(
|
|
`INSERT INTO sent_emails (id, to, subject, type, email_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
).bind(
|
|
crypto.randomUUID(),
|
|
input.to,
|
|
'Reset your password',
|
|
'password_reset',
|
|
data.id,
|
|
new Date().toISOString()
|
|
).run();
|
|
}
|
|
|
|
return { success: true, emailId: data.id };
|
|
} catch (error) {
|
|
console.error('Email send error:', error);
|
|
|
|
// Store failed email for retry
|
|
if (env.DB) {
|
|
await env.DB.prepare(
|
|
`INSERT INTO failed_emails (id, to, subject, type, error, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
).bind(
|
|
crypto.randomUUID(),
|
|
input.to,
|
|
'Reset your password',
|
|
'password_reset',
|
|
error instanceof Error ? error.message : 'Unknown error',
|
|
new Date().toISOString()
|
|
).run();
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
error: 'Email delivery failed. Please try again later.',
|
|
};
|
|
}
|
|
}
|
|
);
|
|
```
|
|
|
|
**Generate File**: `server/emails/send-newsletter.ts`
|
|
|
|
```typescript
|
|
import { createServerFn } from '@tanstack/start';
|
|
import { Resend } from 'resend';
|
|
import { Newsletter } from '@/emails/newsletter';
|
|
|
|
interface NewsletterArticle {
|
|
title: string;
|
|
description: string;
|
|
url: string;
|
|
}
|
|
|
|
interface SendNewsletterInput {
|
|
to: string[];
|
|
month: string;
|
|
articles: NewsletterArticle[];
|
|
unsubscribeBaseUrl: string;
|
|
}
|
|
|
|
export const sendNewsletter = createServerFn(
|
|
'POST',
|
|
async (input: SendNewsletterInput, context) => {
|
|
const { env } = context.cloudflare;
|
|
const resend = new Resend(env.RESEND_API_KEY);
|
|
|
|
try {
|
|
// Batch send using Resend's batch API
|
|
const batch = input.to.map(email => ({
|
|
from: 'newsletter@yourdomain.com',
|
|
to: email,
|
|
subject: `${input.month} Newsletter - Latest updates`,
|
|
react: Newsletter({
|
|
month: input.month,
|
|
articles: input.articles,
|
|
unsubscribeUrl: `${input.unsubscribeBaseUrl}?email=${encodeURIComponent(email)}`,
|
|
}),
|
|
}));
|
|
|
|
const { data, error } = await resend.batch.send(batch);
|
|
|
|
if (error) {
|
|
console.error('Batch send error:', error);
|
|
throw new Error(`Failed to send newsletters: ${error.message}`);
|
|
}
|
|
|
|
// Log sent emails
|
|
if (env.DB) {
|
|
for (const email of input.to) {
|
|
await env.DB.prepare(
|
|
`INSERT INTO sent_emails (id, to, subject, type, email_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
).bind(
|
|
crypto.randomUUID(),
|
|
email,
|
|
`${input.month} Newsletter`,
|
|
'newsletter',
|
|
data?.id || 'batch',
|
|
new Date().toISOString()
|
|
).run();
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
sent: input.to.length,
|
|
batchId: data?.id,
|
|
};
|
|
} catch (error) {
|
|
console.error('Newsletter send error:', error);
|
|
|
|
// Store failed batch
|
|
if (env.DB) {
|
|
for (const email of input.to) {
|
|
await env.DB.prepare(
|
|
`INSERT INTO failed_emails (id, to, subject, type, error, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
).bind(
|
|
crypto.randomUUID(),
|
|
email,
|
|
`${input.month} Newsletter`,
|
|
'newsletter',
|
|
error instanceof Error ? error.message : 'Unknown error',
|
|
new Date().toISOString()
|
|
).run();
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
error: 'Newsletter delivery failed. Please try again later.',
|
|
};
|
|
}
|
|
}
|
|
);
|
|
```
|
|
|
|
### 5. Generate Database Migration for Email Tracking
|
|
|
|
**Generate File**: `migrations/0001_email_tracking.sql`
|
|
|
|
```sql
|
|
-- Sent emails log (audit trail)
|
|
CREATE TABLE sent_emails (
|
|
id TEXT PRIMARY KEY,
|
|
to TEXT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
type TEXT NOT NULL, -- 'verification', 'password_reset', 'newsletter', 'custom'
|
|
email_id TEXT UNIQUE, -- Resend email ID for tracking
|
|
opened INTEGER DEFAULT 0,
|
|
clicked INTEGER DEFAULT 0,
|
|
bounced INTEGER DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
-- Failed emails for retry logic
|
|
CREATE TABLE failed_emails (
|
|
id TEXT PRIMARY KEY,
|
|
to TEXT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
error TEXT,
|
|
retry_count INTEGER DEFAULT 0,
|
|
last_retry_at TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
-- Indexes for performance
|
|
CREATE INDEX idx_sent_emails_to ON sent_emails(to);
|
|
CREATE INDEX idx_sent_emails_type ON sent_emails(type);
|
|
CREATE INDEX idx_sent_emails_created ON sent_emails(created_at);
|
|
CREATE INDEX idx_failed_emails_to ON failed_emails(to);
|
|
CREATE INDEX idx_failed_emails_type ON failed_emails(type);
|
|
CREATE INDEX idx_failed_emails_retry_count ON failed_emails(retry_count);
|
|
```
|
|
|
|
**Run Migration**:
|
|
```bash
|
|
wrangler d1 migrations apply DB --local
|
|
wrangler d1 migrations apply DB --remote
|
|
```
|
|
|
|
### 6. Configure Environment Variables
|
|
|
|
**Update**: `wrangler.toml`
|
|
|
|
```toml
|
|
# Add Resend API key binding
|
|
[env.production.vars]
|
|
# RESEND_API_KEY should be set as a secret, not in this file
|
|
|
|
# D1 database binding (if using email tracking)
|
|
[[d1_databases]]
|
|
binding = "DB"
|
|
database_name = "my-app-db"
|
|
database_id = "..." # Get from: wrangler d1 create my-app-db
|
|
```
|
|
|
|
**Create**: `.dev.vars` (local development)
|
|
|
|
```bash
|
|
# Resend API Key (sensitive - DO NOT COMMIT)
|
|
RESEND_API_KEY=re_your_api_key_here
|
|
|
|
# Your domain for email sender (update to your domain)
|
|
# RESEND_FROM_EMAIL=noreply@yourdomain.com
|
|
```
|
|
|
|
**Production Setup**:
|
|
```bash
|
|
# Set Resend API key as a secret
|
|
wrangler secret put RESEND_API_KEY
|
|
# Paste: re_xxxxxxxxxxxxx
|
|
```
|
|
|
|
### 7. Configure Domain Verification
|
|
|
|
**Instructions for User**:
|
|
|
|
```markdown
|
|
## Setup Custom Email Domain (Required for Production)
|
|
|
|
### Step 1: Add Domain in Resend
|
|
1. Go to https://resend.com/dashboard/domains
|
|
2. Click "Add Domain"
|
|
3. Enter your domain (e.g., yourdomain.com)
|
|
4. Resend will show DNS records to add
|
|
|
|
### Step 2: Add DNS Records
|
|
Add these records to your domain's DNS provider (Cloudflare, Route53, etc.):
|
|
|
|
**SPF Record**:
|
|
```
|
|
Type: TXT
|
|
Name: yourdomain.com
|
|
Value: v=spf1 include:resend.com ~all
|
|
```
|
|
|
|
**DKIM Record**:
|
|
```
|
|
Type: CNAME
|
|
Name: default._domainkey.yourdomain.com
|
|
Value: [value from Resend dashboard]
|
|
```
|
|
|
|
**DMARC Record** (optional but recommended):
|
|
```
|
|
Type: TXT
|
|
Name: _dmarc.yourdomain.com
|
|
Value: v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com
|
|
```
|
|
|
|
### Step 3: Verify Domain
|
|
1. Return to Resend dashboard
|
|
2. Click "Verify Domain"
|
|
3. Wait for DNS propagation (usually 5-30 minutes)
|
|
4. Once verified, use `from: 'noreply@yourdomain.com'` in emails
|
|
|
|
### Step 4: Update Email Templates
|
|
Replace `noreply@yourdomain.com` with your verified domain across all email functions.
|
|
|
|
### Development
|
|
For development/testing only, use:
|
|
```
|
|
from: 'onboarding@resend.dev'
|
|
```
|
|
This works without domain verification but is limited to emails you've verified with Resend.
|
|
```
|
|
|
|
### 8. Setup Email Retry Logic
|
|
|
|
**Generate File**: `server/utils/email-retry.ts`
|
|
|
|
```typescript
|
|
import { Resend } from 'resend';
|
|
|
|
interface Env {
|
|
RESEND_API_KEY: string;
|
|
DB: D1Database;
|
|
}
|
|
|
|
export async function retryFailedEmails(env: Env) {
|
|
const resend = new Resend(env.RESEND_API_KEY);
|
|
|
|
// Get failed emails that haven't exceeded retry limit
|
|
const failed = await env.DB.prepare(
|
|
`SELECT * FROM failed_emails
|
|
WHERE retry_count < 3
|
|
AND (last_retry_at IS NULL OR datetime(last_retry_at) < datetime('now', '-1 hour'))
|
|
LIMIT 10`
|
|
).all();
|
|
|
|
if (!failed.results || failed.results.length === 0) {
|
|
console.log('No emails to retry');
|
|
return { retried: 0, succeeded: 0, failed: 0 };
|
|
}
|
|
|
|
let succeeded = 0;
|
|
let retryFailed = 0;
|
|
|
|
for (const email of failed.results) {
|
|
try {
|
|
// Reconstruct and resend based on type
|
|
// This is a simplified version - you may need to store template data
|
|
const { error } = await resend.emails.send({
|
|
from: 'noreply@yourdomain.com',
|
|
to: email.to,
|
|
subject: email.subject,
|
|
html: `<p>Retry: ${email.subject}</p>`,
|
|
});
|
|
|
|
if (error) {
|
|
// Increment retry count and update last_retry_at
|
|
await env.DB.prepare(
|
|
`UPDATE failed_emails
|
|
SET retry_count = retry_count + 1,
|
|
last_retry_at = ?
|
|
WHERE id = ?`
|
|
).bind(new Date().toISOString(), email.id).run();
|
|
|
|
retryFailed++;
|
|
} else {
|
|
// Move to sent_emails and remove from failed_emails
|
|
await env.DB.prepare(
|
|
`DELETE FROM failed_emails WHERE id = ?`
|
|
).bind(email.id).run();
|
|
|
|
succeeded++;
|
|
}
|
|
} catch (error) {
|
|
console.error('Retry error for email:', email.id, error);
|
|
retryFailed++;
|
|
}
|
|
}
|
|
|
|
return {
|
|
retried: failed.results.length,
|
|
succeeded,
|
|
failed: retryFailed,
|
|
};
|
|
}
|
|
```
|
|
|
|
**Setup Scheduled Retry** (using Cloudflare Cron):
|
|
|
|
Update `wrangler.toml`:
|
|
```toml
|
|
[[triggers.crons]]
|
|
crons = ["0 */6 * * *"] # Run every 6 hours
|
|
```
|
|
|
|
Create `src/scheduled.ts`:
|
|
```typescript
|
|
export default {
|
|
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
|
|
ctx.waitUntil(
|
|
(async () => {
|
|
const result = await retryFailedEmails(env);
|
|
console.log('Email retry results:', result);
|
|
})()
|
|
);
|
|
},
|
|
};
|
|
```
|
|
|
|
### 9. Integrate with Authentication
|
|
|
|
**Example: Send verification email on signup**
|
|
|
|
Update `server/routes/auth/register.ts`:
|
|
```typescript
|
|
import { sendVerifyEmail } from '@/server/emails/send-verify-email';
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const { email, password } = await readBody(event);
|
|
|
|
// Create user...
|
|
const user = await createUser(email, password, event.context.cloudflare.env.DB);
|
|
|
|
// Generate verification token
|
|
const token = generateToken(user.id);
|
|
|
|
// Send verification email
|
|
const verifyUrl = `${process.env.PUBLIC_URL}/verify?token=${token}`;
|
|
await sendVerifyEmail({
|
|
to: email,
|
|
verificationUrl: verifyUrl,
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
message: 'Account created. Please check your email to verify.',
|
|
};
|
|
});
|
|
```
|
|
|
|
**Example: Send password reset email**
|
|
|
|
Create `server/routes/auth/forgot-password.ts`:
|
|
```typescript
|
|
import { sendPasswordReset } from '@/server/emails/send-password-reset';
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const { email } = await readBody(event);
|
|
|
|
// Find user
|
|
const user = await findUserByEmail(email, event.context.cloudflare.env.DB);
|
|
|
|
if (!user) {
|
|
// Don't reveal if email exists (security)
|
|
return {
|
|
success: true,
|
|
message: 'If an account exists, a reset link has been sent.',
|
|
};
|
|
}
|
|
|
|
// Generate reset token
|
|
const token = generateToken(user.id, '1h');
|
|
|
|
// Send password reset email
|
|
const resetUrl = `${process.env.PUBLIC_URL}/reset-password?token=${token}`;
|
|
await sendPasswordReset({
|
|
to: email,
|
|
resetUrl,
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
message: 'Password reset email sent.',
|
|
};
|
|
});
|
|
```
|
|
|
|
### 10. Setup Email Analytics Webhook (Optional)
|
|
|
|
**Resend Webhook Configuration**:
|
|
|
|
1. Go to https://resend.com/dashboard/settings/webhooks
|
|
2. Add webhook endpoint: `https://yourdomain.com/api/webhooks/email`
|
|
3. Subscribe to events:
|
|
- `email.sent`
|
|
- `email.opened`
|
|
- `email.clicked`
|
|
- `email.bounced`
|
|
|
|
**Create Handler**: `server/api/webhooks/email.ts`
|
|
|
|
```typescript
|
|
export default defineEventHandler(async (event) => {
|
|
const body = await readBody(event);
|
|
const { type, data } = body;
|
|
|
|
try {
|
|
switch (type) {
|
|
case 'email.sent':
|
|
await updateEmailStatus(data.email_id, 'sent', event.context.cloudflare.env.DB);
|
|
break;
|
|
|
|
case 'email.opened':
|
|
await updateEmailStatus(data.email_id, 'opened', event.context.cloudflare.env.DB);
|
|
break;
|
|
|
|
case 'email.clicked':
|
|
await updateEmailStatus(data.email_id, 'clicked', event.context.cloudflare.env.DB);
|
|
break;
|
|
|
|
case 'email.bounced':
|
|
await handleBounce(data.email_id, data.email, event.context.cloudflare.env.DB);
|
|
break;
|
|
|
|
default:
|
|
console.log('Unknown email event:', type);
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Webhook error:', error);
|
|
return { success: false, error: String(error) };
|
|
}
|
|
});
|
|
|
|
async function updateEmailStatus(emailId: string, status: string, db: D1Database) {
|
|
await db.prepare(
|
|
`UPDATE sent_emails SET ${status} = 1, updated_at = ? WHERE email_id = ?`
|
|
).bind(new Date().toISOString(), emailId).run();
|
|
}
|
|
|
|
async function handleBounce(emailId: string, email: string, db: D1Database) {
|
|
await db.prepare(
|
|
`UPDATE sent_emails SET bounced = 1, updated_at = ? WHERE email_id = ?`
|
|
).bind(new Date().toISOString(), emailId).run();
|
|
|
|
// Optionally mark email as invalid for future use
|
|
// await db.prepare(
|
|
// `INSERT INTO invalid_emails (email) VALUES (?) ON CONFLICT DO NOTHING`
|
|
// ).bind(email).run();
|
|
}
|
|
```
|
|
|
|
### 11. Testing Email Flows
|
|
|
|
**Test Email Sending**:
|
|
```typescript
|
|
// Use Resend's test address
|
|
const { data, error } = await resend.emails.send({
|
|
from: 'test@yourdomain.com',
|
|
to: 'test@resend.dev', // Special test address
|
|
subject: 'Test email',
|
|
react: VerifyEmail({
|
|
verificationUrl: 'https://example.com/verify?token=test',
|
|
email: 'test@resend.dev',
|
|
}),
|
|
});
|
|
```
|
|
|
|
**Playwright E2E Test Example**:
|
|
```typescript
|
|
test('sends verification email on signup', async ({ page }) => {
|
|
await page.goto('/signup');
|
|
|
|
await page.fill('[name="email"]', 'test@example.com');
|
|
await page.fill('[name="password"]', 'Test123!@#');
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Verify success message
|
|
await expect(page.locator('[data-testid="success-message"]'))
|
|
.toContainText('Check your email');
|
|
|
|
// In real scenario, verify email was sent via Resend API
|
|
// This test verifies the UI flow, not actual email delivery
|
|
});
|
|
```
|
|
|
|
**Preview React Email Templates**:
|
|
```bash
|
|
# Start React Email preview server
|
|
npm run email:preview
|
|
|
|
# Open http://localhost:3000 to preview templates
|
|
```
|
|
|
|
## Success Criteria
|
|
|
|
✅ Email setup complete when:
|
|
- Resend SDK and React Email dependencies installed
|
|
- All email templates generated (verification, password reset, newsletter)
|
|
- Server functions created for sending emails
|
|
- Database migration for email tracking
|
|
- Environment variables configured
|
|
- Domain verification setup (at least documented)
|
|
- Error handling and retry logic implemented
|
|
- Integration with auth flows completed
|
|
- Email previews working
|
|
- Testing strategy documented
|
|
|
|
## Output Summary
|
|
|
|
**Files Created**:
|
|
- Email templates:
|
|
- `emails/verify-email.tsx`
|
|
- `emails/password-reset.tsx`
|
|
- `emails/newsletter.tsx`
|
|
- Server functions:
|
|
- `server/emails/send-verify-email.ts`
|
|
- `server/emails/send-password-reset.ts`
|
|
- `server/emails/send-newsletter.ts`
|
|
- Database migration: `migrations/0001_email_tracking.sql`
|
|
- Utilities: `server/utils/email-retry.ts`
|
|
- Webhook handler: `server/api/webhooks/email.ts`
|
|
- Scheduled job: `src/scheduled.ts`
|
|
|
|
**Files Updated**:
|
|
- `wrangler.toml` (Resend vars, D1 binding)
|
|
- `.dev.vars` (template)
|
|
|
|
**Next Actions**:
|
|
1. Install dependencies: `npm install resend @react-email/components`
|
|
2. Run database migration: `wrangler d1 migrations apply DB`
|
|
3. Generate Resend API key: https://resend.com/dashboard
|
|
4. Add to secrets: `wrangler secret put RESEND_API_KEY`
|
|
5. Configure domain in Resend dashboard
|
|
6. Test email flows with signup
|
|
7. Preview templates: `npm run email:preview`
|
|
8. Deploy with `/es-deploy`
|
|
|
|
## Notes
|
|
|
|
- Always use Resend for transactional and marketing emails
|
|
- React Email provides type-safe, component-based templates
|
|
- Store RESEND_API_KEY as Cloudflare secret (not in wrangler.toml)
|
|
- Domain verification required for production (use onboarding@resend.dev for testing)
|
|
- Email retry logic handles transient failures
|
|
- Track email opens/clicks via Resend webhooks
|
|
- Test with onboarding@resend.dev before using custom domain
|
|
- Use batch API for newsletters to multiple recipients
|
|
- See `agents/integrations/resend-email-specialist` for detailed guidance
|