api-email-resend-react-email
Resend + React Email templates
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-email-resend-react-email/skills/api-email-resend-react-email
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Email Patterns with Resend and React Email
Quick Guide: Use Resend for transactional emails with React Email templates. Always
await render()before sending (it returns a Promise). Server-side only - never expose API keys to clients. Implement retry with exponential backoff for transient failures. Include unsubscribe links in non-transactional emails (CAN-SPAM). Useresend.batch.send()for 2-100 recipients (no attachments or scheduling support in batch). React Email 5.0+ deprecatedrenderAsync- userender()instead. Webhook verification requires raw request body andwebhookSecretparameter.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST await render() before passing HTML to resend.emails.send() - render returns a Promise)
(You MUST handle Resend API errors and implement retry logic for transient failures)
(You MUST use server-side sending for all emails - never expose RESEND_API_KEY to the client)
(You MUST include unsubscribe links in marketing/notification emails - required for CAN-SPAM compliance)
(You MUST use typed props interfaces for all email templates - enables compile-time validation)
</critical_requirements>
Auto-detection: Resend, React Email, @react-email/components, resend.emails.send, email template, transactional email, verification email, password reset email, notification email, email rendering, resend.batch.send, resend.webhooks.verify
When to use:
- Sending transactional emails (verification, password reset, receipts)
- Creating React Email templates with Tailwind styling
- Building notification systems with email delivery
- Implementing email tracking via webhooks
- Batch sending to multiple recipients
When NOT to use:
- Marketing campaign management (use dedicated marketing tools)
- SMS or push notifications (different services)
- Email list management (use Resend Audiences or marketing tools)
Detailed Resources:
- examples/core.md - Template structure, sending with error handling, retry logic
- examples/async-batch.md - Async sending, batch API
- examples/webhooks.md - Webhook handler with signature verification
- examples/templates.md - Password Reset, Notification templates
- examples/preferences.md - Unsubscribe, email preferences
- examples/advanced-features.md - Scheduled sending, idempotency keys, tags
- reference.md - Decision frameworks, anti-patterns
<red_flags>
RED FLAGS
High Priority Issues:
- Not awaiting
render()- sends"[object Promise]"as email body - API key exposed on client - security vulnerability
- No error handling - silent failures
- Missing unsubscribe links in non-transactional emails - CAN-SPAM violation
- Sending without checking user preferences - spam
Medium Priority Issues:
- No retry logic for transient failures (rate limits, 5xx errors)
- Sync sending blocking request handlers for non-critical emails
- Hardcoded from address instead of environment variable
- No webhook verification signature check
- Not logging email send results
Common Mistakes:
- Using
GridorFlexboxin email templates (not supported by email clients) - Expecting shadows or gradients to render in emails
- Using
remunits (email clients handle differently) - Forgetting
PreviewPropsfor dev server
Gotchas & Edge Cases:
- Resend SDK accepts a
reactprop directly (renders internally), but pre-rendering withawait render()+htmlgives you control over the output and works outside the Resend SDK render()is async in React Email 5.0+ (renderAsyncdeprecated)- Batch API limited to 100 emails, does NOT support
attachmentsorscheduledAt - Webhooks require raw request body - JSON parsing breaks signature verification
- Webhook verify uses
webhookSecretparameter (notsecret) - Webhook headers object uses short keys:
id,timestamp,signature - Idempotency keys are passed as a second argument to
resend.emails.send(), not in the email payload headers - Idempotency keys expire after 24 hours, max 256 characters
- Tags: ASCII alphanumeric, underscores, dashes only, max 256 chars per key/value
- Tailwind in emails requires
@react-email/tailwindwrapper (Tailwind 4 supported in React Email 5.0+) - Images must use absolute URLs (no relative paths)
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST await render() before passing HTML to resend.emails.send() - render returns a Promise)
(You MUST handle Resend API errors and implement retry logic for transient failures)
(You MUST use server-side sending for all emails - never expose RESEND_API_KEY to the client)
(You MUST include unsubscribe links in marketing/notification emails - required for CAN-SPAM compliance)
(You MUST use typed props interfaces for all email templates - enables compile-time validation)
Failure to follow these rules will cause email delivery failures, security vulnerabilities, or legal compliance issues.
</critical_reminders>
Sources
Files (skills)
-
examples
-
advanced-features.md 6.8 KB
# Email - Advanced Features Examples > Scheduled sending, idempotency keys, and tags. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for basic send pattern. --- ## Pattern 1: Scheduled Email Sending Send emails at a future time using the `scheduledAt` parameter. ```typescript // lib/email/scheduled-email.ts import { Resend } from "resend"; import { render } from "@react-email/components"; const resend = new Resend(process.env.RESEND_API_KEY); const MAX_SCHEDULE_DAYS = 30; // Resend allows scheduling up to 30 days in advance interface ScheduledEmailOptions { to: string | string[]; subject: string; react: React.ReactElement; scheduledAt: Date; tags?: Array<{ name: string; value: string }>; } export async function sendScheduledEmail( options: ScheduledEmailOptions, ): Promise<{ success: boolean; id?: string; error?: string }> { // Validate scheduling window (up to 30 days in advance) const now = new Date(); const maxScheduleDate = new Date( now.getTime() + MAX_SCHEDULE_DAYS * 24 * 60 * 60 * 1000, ); if (options.scheduledAt <= now) { return { success: false, error: "Scheduled time must be in the future" }; } if (options.scheduledAt > maxScheduleDate) { return { success: false, error: `Cannot schedule more than ${MAX_SCHEDULE_DAYS} days in advance`, }; } try { const html = await render(options.react); const { data, error } = await resend.emails.send({ from: `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`, to: options.to, subject: options.subject, html, scheduledAt: options.scheduledAt.toISOString(), tags: options.tags, }); if (error) { console.error("[Email] Scheduled send failed:", error); return { success: false, error: error.message }; } console.log("[Email] Scheduled:", data?.id, "for", options.scheduledAt); return { success: true, id: data?.id }; } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; return { success: false, error: message }; } } export type { ScheduledEmailOptions }; ``` **Why good:** Validates scheduling window (up to 30 days), converts Date to ISO string for API, supports tags with scheduled emails --- ## Pattern 2: Idempotency Keys Prevent duplicate email sends using idempotency keys. ```typescript // lib/email/idempotent-email.ts import { Resend } from "resend"; import { render } from "@react-email/components"; const resend = new Resend(process.env.RESEND_API_KEY); const IDEMPOTENCY_KEY_MAX_LENGTH = 256; interface IdempotentEmailOptions { to: string | string[]; subject: string; react: React.ReactElement; idempotencyKey: string; } export async function sendIdempotentEmail( options: IdempotentEmailOptions, ): Promise<{ success: boolean; id?: string; error?: string; isDuplicate?: boolean; }> { if (options.idempotencyKey.length > IDEMPOTENCY_KEY_MAX_LENGTH) { return { success: false, error: `Idempotency key must be ${IDEMPOTENCY_KEY_MAX_LENGTH} characters or less`, }; } try { const html = await render(options.react); // Idempotency key is passed as a second argument, not in the email payload const { data, error } = await resend.emails.send( { from: `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`, to: options.to, subject: options.subject, html, }, { idempotencyKey: options.idempotencyKey, }, ); if (error) { const errorName = error.name?.toLowerCase() ?? ""; if (errorName.includes("invalid_idempotent_request")) { return { success: false, error: "This idempotency key was already used with different payload", isDuplicate: true, }; } if (errorName.includes("concurrent_idempotent_requests")) { return { success: false, error: "Another request with this idempotency key is in progress", isDuplicate: true, }; } return { success: false, error: error.message }; } return { success: true, id: data?.id }; } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; return { success: false, error: message }; } } export type { IdempotentEmailOptions }; ``` **Why good:** Validates key length, handles idempotency-specific errors, returns `isDuplicate` flag for caller handling --- ## Pattern 3: Usage - Idempotent Order Confirmation Use order ID as idempotency key to prevent duplicate confirmation emails. ```typescript import { sendIdempotentEmail } from "./lib/email/idempotent-email"; import { OrderConfirmationEmail } from "./templates/order-confirmation"; async function sendOrderConfirmation(order: { id: string; userEmail: string; userName: string; total: number; }) { // Use order ID as idempotency key - guarantees one email per order const result = await sendIdempotentEmail({ to: order.userEmail, subject: `Order #${order.id} confirmed`, react: OrderConfirmationEmail({ userName: order.userName, orderId: order.id, total: order.total, }), idempotencyKey: `order-confirmation-${order.id}`, }); if (result.isDuplicate) { console.log("[Order] Confirmation already sent for:", order.id); } return result; } ``` --- ## Pattern 4: Email Tags for Tracking Add metadata tags to emails for analytics and organization. ```typescript // Tags are passed directly to resend.emails.send() const { data, error } = await resend.emails.send({ from, to, subject, html, tags: [ { name: "campaign_id", value: campaign.id }, { name: "email_type", value: "newsletter" }, { name: "ab_variant", value: "A" }, ], }); ``` **Tag constraints:** - Names and values: max 256 characters each - Characters: ASCII alphanumeric, underscores, dashes only (`/^[a-zA-Z0-9_-]+$/`) - Supported in both single send and batch API - Supported with scheduled emails --- ## Feature Comparison | Feature | Use Case | Limit | Batch Support | | ----------------- | --------------------------------- | --------------------------- | ------------- | | Scheduled sending | Reminders, time-zone aware | Up to 30 days in advance | Not supported | | Idempotency keys | Prevent duplicates, retry safety | 256 chars, expires 24 hours | Supported | | Tags | Analytics, filtering, A/B testing | 256 chars per key/value | Supported | --- ## Notes - **Scheduled emails** cannot be used with batch API - **Attachments** cannot be used with batch API - **Idempotency keys** expire after 24 hours - **Tags** support ASCII alphanumeric characters, underscores, and dashes only - All features work with the standard `resend.emails.send()` method -
async-batch.md 5.3 KB
# Email - Async and Batch Sending Examples > Non-blocking email sending and batch API patterns. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for basic send pattern. --- ## Pattern 1: Fire and Forget Send emails without blocking the response. ```typescript // lib/email/async-email.ts import type { SendEmailOptions } from "./send-email"; import { sendEmailWithRetry } from "./send-with-retry"; // Queue for tracking in-flight emails (for graceful shutdown) const inFlightEmails = new Set<Promise<unknown>>(); export function sendEmailAsync(options: SendEmailOptions): void { const promise = sendEmailWithRetry(options) .catch((err) => { console.error("[Email] Async send failed:", err); }) .finally(() => { inFlightEmails.delete(promise); }); inFlightEmails.add(promise); } // For graceful shutdown - wait for all emails to complete export async function flushPendingEmails(): Promise<void> { await Promise.all(inFlightEmails); } ``` --- ## Pattern 2: Usage in API Route Non-blocking email in a signup flow. ```typescript // api/signup/route.ts import { sendEmailAsync } from "./lib/email/async-email"; import { WelcomeEmail } from "./templates/welcome-email"; // Adapt to your web framework's route handler export async function handleSignup(request: Request) { const body = await request.json(); // Create user (blocking - required for response) const user = await createUser(body); // Send welcome email asynchronously (non-blocking) sendEmailAsync({ to: user.email, subject: "Welcome to Your App!", react: WelcomeEmail({ userName: user.name ?? "there", loginUrl: `${process.env.APP_URL}/login`, }), }); // Return immediately - email sends in background return Response.json({ success: true, user }); } ``` **Why good:** Signup response is fast (doesn't wait for email), email failures don't break the signup flow, flushPendingEmails enables graceful shutdown --- ## Pattern 3: Batch Email Function Send to multiple recipients efficiently using Resend's batch API. ```typescript // lib/email/batch-email.ts import { Resend } from "resend"; import { render } from "@react-email/components"; const resend = new Resend(process.env.RESEND_API_KEY); const MAX_BATCH_SIZE = 100; // Resend limit per batch request const DEFAULT_FROM = `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`; interface BatchEmailItem { to: string; subject: string; react: React.ReactElement; } interface BatchSendResult { success: boolean; data?: { id: string }[]; errors?: string[]; } export async function sendBatchEmails( emails: BatchEmailItem[], ): Promise<BatchSendResult> { const results: { id: string }[] = []; const errors: string[] = []; // Split into batches of MAX_BATCH_SIZE for (let i = 0; i < emails.length; i += MAX_BATCH_SIZE) { const batch = emails.slice(i, i + MAX_BATCH_SIZE); // Render all emails in batch in parallel const renderedEmails = await Promise.all( batch.map(async (email) => ({ from: DEFAULT_FROM, to: email.to, subject: email.subject, html: await render(email.react), })), ); try { const { data, error } = await resend.batch.send(renderedEmails); if (error) { errors.push( `Batch ${Math.floor(i / MAX_BATCH_SIZE) + 1}: ${error.message}`, ); } else if (data) { results.push(...data); } } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; errors.push(`Batch ${Math.floor(i / MAX_BATCH_SIZE) + 1}: ${message}`); } } return { success: errors.length === 0, data: results.length > 0 ? results : undefined, errors: errors.length > 0 ? errors : undefined, }; } export type { BatchEmailItem, BatchSendResult }; ``` --- ## Pattern 4: Batch Usage Example Send notifications to all team members. ```typescript // Send notifications to all team members import { sendBatchEmails } from "./lib/email/batch-email"; import { NotificationEmail } from "./templates/notification-email"; async function notifyTeamMembers( members: { email: string; name: string }[], notification: { title: string; body: string; actionUrl: string }, ) { const emails = members.map((member) => ({ to: member.email, subject: notification.title, react: NotificationEmail({ userName: member.name, notificationType: "update" as const, title: notification.title, body: notification.body, actionUrl: notification.actionUrl, actionText: "View Update", unsubscribeUrl: `${process.env.APP_URL}/unsubscribe?email=${member.email}`, }), })); const result = await sendBatchEmails(emails); if (!result.success) { console.error("[Email] Batch send had errors:", result.errors); } return result; } ``` **Why good:** Handles Resend's 100-email batch limit, renders all templates in parallel, returns detailed results per batch --- ## Batch API Limitations **Not Supported in Batch:** - `attachments` field - use single send for emails with attachments - `scheduledAt` field - use single send for scheduled emails **Supported in Batch:** - `tags` - for analytics and tracking - `headers` (custom email headers) - `cc` and `bcc` recipients - Up to 50 recipients per email (`to` field) -
auth-integration.md 888 B
# Email - Authentication Integration > **This file has been removed.** Auth integration patterns belong in your authentication skill/documentation, not in the email skill. **Key guidance:** Wire your auth system's email callbacks (verification, password reset) to the `sendEmailWithRetry()` function from [core.md](core.md) Pattern 3. The email skill handles sending; your auth system handles when to trigger it. ```typescript // In your auth configuration, connect callbacks to the email send function: async function sendVerificationEmail( user: { email: string; name?: string }, url: string, ) { await sendEmailWithRetry({ to: user.email, subject: "Verify your email address", react: VerificationEmail({ userName: user.name ?? "there", verificationUrl: url, }), }); } ``` See [core.md](core.md) for the full `sendEmailWithRetry` implementation. -
core.md 7.6 KB
# Email - Core Examples > Essential email patterns - template structure, sending, and retry. See [SKILL.md](../SKILL.md) for core concepts. **Extended Examples:** - [templates.md](templates.md) - Password Reset, Notification Templates - [async-batch.md](async-batch.md) - Async Sending, Batch API - [webhooks.md](webhooks.md) - Webhook Handler for Tracking - [preferences.md](preferences.md) - Unsubscribe, Email Preferences - [advanced-features.md](advanced-features.md) - Scheduled Sending, Idempotency Keys, Tags --- ## Pattern 1: Email Template Structure Create well-structured email templates with proper typing. ```typescript // templates/welcome-email.tsx import { Button, Heading, Link, Text } from "@react-email/components"; import { BaseLayout } from "../layouts/base-layout"; const CTA_PADDING_X = 24; const CTA_PADDING_Y = 12; // Always define props interface interface WelcomeEmailProps { userName: string; loginUrl: string; features?: string[]; } export function WelcomeEmail({ userName, loginUrl, features = [], }: WelcomeEmailProps) { return ( <BaseLayout preview={`Welcome to Your App, ${userName}!`}> <Heading className="text-2xl font-bold text-gray-900 mb-4"> Welcome to Your App! </Heading> <Text className="text-gray-600 mb-4">Hi {userName},</Text> <Text className="text-gray-600 mb-6"> Thanks for joining! We're excited to have you on board. </Text> {features.length > 0 && ( <> <Text className="text-gray-600 mb-2 font-semibold"> Here's what you can do: </Text> <ul className="text-gray-600 mb-6 pl-4"> {features.map((feature) => ( <li key={feature} className="mb-1"> {feature} </li> ))} </ul> </> )} <Button href={loginUrl} className="bg-blue-600 text-white font-semibold rounded-md" style={{ paddingLeft: CTA_PADDING_X, paddingRight: CTA_PADDING_X, paddingTop: CTA_PADDING_Y, paddingBottom: CTA_PADDING_Y, }} > Get Started </Button> </BaseLayout> ); } // Preview props for development server WelcomeEmail.PreviewProps = { userName: "John", loginUrl: "https://example.com/login", features: ["Create projects", "Invite team members", "Track progress"], } satisfies WelcomeEmailProps; export type { WelcomeEmailProps }; ``` **Why good:** Typed props catch errors at compile time, PreviewProps enable dev server preview, optional props have defaults, BaseLayout ensures consistency --- ## Pattern 2: Basic Send with Error Handling Send emails with proper error handling and logging. ```typescript // lib/email/send-email.ts import { Resend } from "resend"; import { render } from "@react-email/components"; const resend = new Resend(process.env.RESEND_API_KEY); const DEFAULT_FROM_NAME = "Your App"; const DEFAULT_FROM_ADDRESS = "noreply@yourdomain.com"; interface SendEmailOptions { to: string | string[]; subject: string; react: React.ReactElement; replyTo?: string; cc?: string[]; bcc?: string[]; } interface SendEmailResult { success: boolean; id?: string; error?: string; } export async function sendEmail( options: SendEmailOptions, ): Promise<SendEmailResult> { try { // CRITICAL: Always await render() const html = await render(options.react); const { data, error } = await resend.emails.send({ from: `${DEFAULT_FROM_NAME} <${DEFAULT_FROM_ADDRESS}>`, to: options.to, subject: options.subject, html, replyTo: options.replyTo, cc: options.cc, bcc: options.bcc, }); if (error) { console.error("[Email] Send failed:", error); return { success: false, error: error.message }; } console.log("[Email] Sent successfully:", data?.id); return { success: true, id: data?.id }; } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; console.error("[Email] Unexpected error:", message); return { success: false, error: message }; } } export type { SendEmailOptions, SendEmailResult }; ``` **Why good:** Wraps Resend client with consistent interface, always awaits render(), returns typed result, logs for debugging --- ## Pattern 3: Retry with Exponential Backoff Implement retry logic for temporary API failures. ```typescript // lib/email/constants.ts export const MAX_RETRY_ATTEMPTS = 3; export const INITIAL_RETRY_DELAY_MS = 1000; export const RETRY_BACKOFF_MULTIPLIER = 2; // Errors that are safe to retry const RETRYABLE_ERRORS = [ "rate_limit_exceeded", "internal_server_error", "service_unavailable", ]; ``` ```typescript // lib/email/send-with-retry.ts import { Resend } from "resend"; import { render } from "@react-email/components"; import { MAX_RETRY_ATTEMPTS, INITIAL_RETRY_DELAY_MS, RETRY_BACKOFF_MULTIPLIER, } from "./constants"; interface SendWithRetryOptions { to: string | string[]; subject: string; react: React.ReactElement; maxRetries?: number; } async function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } export async function sendEmailWithRetry( options: SendWithRetryOptions, ): Promise<{ success: boolean; id?: string; error?: string }> { const resend = new Resend(process.env.RESEND_API_KEY); const maxRetries = options.maxRetries ?? MAX_RETRY_ATTEMPTS; let lastError: string | undefined; let attempt = 0; while (attempt < maxRetries) { attempt++; try { const html = await render(options.react); const { data, error } = await resend.emails.send({ from: `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`, to: options.to, subject: options.subject, html, }); if (error) { lastError = error.message; const isRetryable = RETRYABLE_ERRORS.some((e) => error.name?.toLowerCase().includes(e), ); if (isRetryable && attempt < maxRetries) { const delay = INITIAL_RETRY_DELAY_MS * Math.pow(RETRY_BACKOFF_MULTIPLIER, attempt - 1); console.log( `[Email] Retry ${attempt}/${maxRetries} after ${delay}ms`, ); await sleep(delay); continue; } return { success: false, error: error.message }; } return { success: true, id: data?.id }; } catch (err) { lastError = err instanceof Error ? err.message : "Unknown error"; if (attempt < maxRetries) { const delay = INITIAL_RETRY_DELAY_MS * Math.pow(RETRY_BACKOFF_MULTIPLIER, attempt - 1); await sleep(delay); continue; } } } return { success: false, error: lastError ?? "Max retries exceeded" }; } ``` **Why good:** Exponential backoff prevents overwhelming the API, only retries transient errors, configurable retry count, logs retry attempts --- ## Anti-Pattern: Common Mistakes ```typescript // BAD Example - Common mistakes async function sendEmailBad( to: string, subject: string, react: React.ReactElement, ) { const resend = new Resend(process.env.RESEND_API_KEY); // BAD: Not awaiting render - sends "[object Promise]" const html = render(react); // BAD: No error handling - silent failures await resend.emails.send({ from: "noreply@example.com", // BAD: Hardcoded to, subject, html, // This is a Promise, not a string! }); // BAD: No return value - caller has no idea if it worked } ``` **Why bad:** Not awaiting render() sends garbage, no error handling means silent failures, hardcoded from address breaks when domain changes -
preferences.md 4.8 KB
# Email - Preferences Examples > Email preferences and unsubscribe handling for CAN-SPAM compliance. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for basic send pattern. > **Database examples below are pseudocode.** Adapt to your ORM/database solution - the patterns remain the same. --- ## Pattern 1: Preferences Schema Store user email preferences in database. ```typescript // lib/db/schema/email-preferences.ts // Adapt to your database solution // Schema concept - implement with your ORM: // table: email_preferences // user_id: text (primary key) // marketing_emails: boolean (default: true) // product_updates: boolean (default: true) // team_notifications: boolean (default: true) // security_alerts: boolean (default: true, cannot be disabled) // updated_at: timestamp interface EmailPreferences { userId: string; marketingEmails: boolean; productUpdates: boolean; teamNotifications: boolean; securityAlerts: boolean; // Cannot be disabled updatedAt: Date; } ``` --- ## Pattern 2: Unsubscribe Endpoint Token-based unsubscribe for security. ```typescript // api/email/unsubscribe/route.ts import jwt from "jsonwebtoken"; const UNSUBSCRIBE_SECRET = process.env.UNSUBSCRIBE_SECRET!; interface UnsubscribeToken { userId: string; category: "marketing" | "product_updates" | "team_notifications"; } // Adapt to your web framework's route handler export async function handleUnsubscribe(request: Request) { const url = new URL(request.url); const token = url.searchParams.get("token"); if (!token) { return Response.redirect("/unsubscribe-error"); } try { const decoded = jwt.verify(token, UNSUBSCRIBE_SECRET) as UnsubscribeToken; // Map category to database column const columnMap = { marketing: "marketingEmails", product_updates: "productUpdates", team_notifications: "teamNotifications", } as const; const column = columnMap[decoded.category]; // Update preference in your database await updateEmailPreference(decoded.userId, column, false); return Response.redirect("/unsubscribe-success"); } catch (err) { return Response.redirect("/unsubscribe-error"); } } ``` --- ## Pattern 3: Generating Unsubscribe URL Create signed unsubscribe tokens. ```typescript // lib/email/unsubscribe.ts import jwt from "jsonwebtoken"; const UNSUBSCRIBE_SECRET = process.env.UNSUBSCRIBE_SECRET!; const UNSUBSCRIBE_TOKEN_EXPIRY = "30d"; type EmailCategory = "marketing" | "product_updates" | "team_notifications"; export function generateUnsubscribeUrl( userId: string, category: EmailCategory, ): string { const token = jwt.sign({ userId, category }, UNSUBSCRIBE_SECRET, { expiresIn: UNSUBSCRIBE_TOKEN_EXPIRY, }); return `${process.env.APP_URL}/api/email/unsubscribe?token=${token}`; } export type { EmailCategory }; ``` **Why good:** Token-based unsubscribe prevents unauthorized changes, security alerts cannot be disabled, preferences stored in database for checking before send --- ## Pattern 4: Checking Preferences Before Sending Respect user preferences for non-transactional emails. ```typescript // lib/email/send-notification.ts import { sendEmail } from "./send-email"; import { generateUnsubscribeUrl } from "./unsubscribe"; import { NotificationEmail } from "../templates/notification-email"; interface NotificationOptions { userId: string; email: string; userName: string; category: "marketing" | "product_updates" | "team_notifications"; title: string; body: string; actionUrl: string; actionText: string; } export async function sendNotificationEmail( options: NotificationOptions, ): Promise<{ sent: boolean; reason?: string }> { // Check user preferences (implement with your database solution) const preferences = await getEmailPreferences(options.userId); // Check if user has opted out of this category const categoryMap = { marketing: preferences?.marketingEmails ?? true, product_updates: preferences?.productUpdates ?? true, team_notifications: preferences?.teamNotifications ?? true, }; if (!categoryMap[options.category]) { return { sent: false, reason: "User has opted out of this category" }; } // Generate unsubscribe URL const unsubscribeUrl = generateUnsubscribeUrl( options.userId, options.category, ); // Send email const result = await sendEmail({ to: options.email, subject: options.title, react: NotificationEmail({ userName: options.userName, notificationType: "update", title: options.title, body: options.body, actionUrl: options.actionUrl, actionText: options.actionText, unsubscribeUrl, }), }); return { sent: result.success, reason: result.error }; } ``` **Why good:** Respects user preferences, includes proper unsubscribe link, returns reason if not sent -
retry.md 1 KB
# Email - Retry Logic Examples > Retry patterns for handling transient email failures. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for basic send pattern. > **Note:** The complete retry implementation is in [core.md](core.md) Pattern 3. This file provides the retry strategy decision tree for quick reference. --- ## Retry Strategy Decision Tree ``` Is the error retryable? +-- Rate limit --> Retry with exponential backoff +-- Server error (5xx) --> Retry with backoff +-- Invalid email --> Don't retry, log error +-- Authentication error --> Don't retry, check API key +-- Quota exceeded --> Don't retry, upgrade plan ``` --- ## Retry Constants ```typescript export const MAX_RETRY_ATTEMPTS = 3; export const INITIAL_RETRY_DELAY_MS = 1000; export const RETRY_BACKOFF_MULTIPLIER = 2; // Errors that are safe to retry const RETRYABLE_ERRORS = [ "rate_limit_exceeded", "internal_server_error", "service_unavailable", ]; ``` The full retry implementation with exponential backoff is in [core.md](core.md) Pattern 3. -
templates.md 4.4 KB
# Email - Template Examples > Template patterns for common email types. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for basic template structure. --- ## Pattern 1: Password Reset Email Security-focused template with expiry messaging. ```typescript // templates/password-reset.tsx import { Button, Heading, Text } from "@react-email/components"; import { BaseLayout } from "../layouts/base-layout"; const CTA_PADDING_X = 24; const CTA_PADDING_Y = 12; const LINK_EXPIRY_MINUTES = 60; interface PasswordResetEmailProps { userName: string; resetUrl: string; } export function PasswordResetEmail({ userName, resetUrl, }: PasswordResetEmailProps) { return ( <BaseLayout preview="Reset your password"> <Heading className="text-2xl font-bold text-gray-900 mb-4"> Reset your password </Heading> <Text className="text-gray-600 mb-4">Hi {userName},</Text> <Text className="text-gray-600 mb-6"> We received a request to reset your password. Click the button below to create a new password. </Text> <Button href={resetUrl} className="bg-blue-600 text-white font-semibold rounded-md" style={{ paddingLeft: CTA_PADDING_X, paddingRight: CTA_PADDING_X, paddingTop: CTA_PADDING_Y, paddingBottom: CTA_PADDING_Y, }} > Reset Password </Button> <Text className="text-sm text-gray-500 mt-6"> This link will expire in {LINK_EXPIRY_MINUTES} minutes. </Text> <Text className="text-sm text-gray-500 mt-4"> If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged. </Text> </BaseLayout> ); } PasswordResetEmail.PreviewProps = { userName: "John", resetUrl: "https://example.com/reset?token=abc123", } satisfies PasswordResetEmailProps; export type { PasswordResetEmailProps }; ``` **Why good:** Clear security messaging, expiry time communicated, reassurance for users who didn't request reset --- ## Pattern 2: Notification Email with Unsubscribe Template with required CAN-SPAM unsubscribe link. ```typescript // templates/notification-email.tsx import { Button, Heading, Link, Text } from "@react-email/components"; import { BaseLayout } from "../layouts/base-layout"; const CTA_PADDING_X = 20; const CTA_PADDING_Y = 10; interface NotificationEmailProps { userName: string; notificationType: "mention" | "comment" | "update"; title: string; body: string; actionUrl: string; actionText: string; unsubscribeUrl: string; } const NOTIFICATION_TITLES: Record<string, string> = { mention: "You were mentioned", comment: "New comment on your post", update: "Project update", }; export function NotificationEmail({ userName, notificationType, title, body, actionUrl, actionText, unsubscribeUrl, }: NotificationEmailProps) { return ( <BaseLayout preview={title}> <Text className="text-sm text-blue-600 font-medium mb-2"> {NOTIFICATION_TITLES[notificationType]} </Text> <Heading className="text-xl font-bold text-gray-900 mb-4"> {title} </Heading> <Text className="text-gray-600 mb-4">Hi {userName},</Text> <Text className="text-gray-600 mb-6">{body}</Text> <Button href={actionUrl} className="bg-blue-600 text-white font-semibold rounded-md" style={{ paddingLeft: CTA_PADDING_X, paddingRight: CTA_PADDING_X, paddingTop: CTA_PADDING_Y, paddingBottom: CTA_PADDING_Y, }} > {actionText} </Button> {/* REQUIRED: Unsubscribe link for CAN-SPAM compliance */} <Text className="text-xs text-gray-400 mt-8 text-center"> <Link href={unsubscribeUrl} className="text-gray-400 underline"> Unsubscribe from these notifications </Link> </Text> </BaseLayout> ); } NotificationEmail.PreviewProps = { userName: "John", notificationType: "mention", title: "Sarah mentioned you in a comment", body: '@John can you review this?', actionUrl: "https://example.com/comments/123", actionText: "View Comment", unsubscribeUrl: "https://example.com/unsubscribe?token=abc", } satisfies NotificationEmailProps; export type { NotificationEmailProps }; ``` **Why good:** Unsubscribe link is required for compliance, notification type enables different styling, props are fully typed -
testing.md 1 KB
# Email - Testing > **Testing guidance:** Use React Email's preview server for visual testing during development. For unit tests, `await render(YourTemplate({ props }))` returns HTML string you can assert against. Mock the Resend client in send function tests. **Preview server:** ```bash # Start React Email preview server for visual testing npx react-email dev # View at http://localhost:3000 ``` **Template testing pattern:** ```typescript // Render template and check output contains expected content const html = await render( WelcomeEmail({ userName: "John", loginUrl: "https://example.com/login" }), ); expect(html).toContain("Welcome"); expect(html).toContain("John"); expect(html).toContain("https://example.com/login"); ``` **Testing checklist:** - All templates render without errors - Required props are validated (TypeScript) - Optional props have sensible defaults - PreviewProps are defined for dev server - Edge cases handled (empty arrays, null values) - Links are properly escaped - Special characters render correctly (`'`, etc.) -
webhooks.md 5 KB
# Email - Webhook Examples > Webhook handler for tracking email events. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for basic send pattern. --- ## Pattern 1: Webhook Handler Using Resend SDK (Recommended) Process Resend webhook events with the built-in SDK verification method. ```typescript // api/webhooks/resend/route.ts import { Resend } from "resend"; const resend = new Resend(process.env.RESEND_API_KEY); interface ResendWebhookPayload { type: | "email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained"; data: { email_id: string; to: string[]; subject: string; created_at: string; click?: { link: string }; }; } // Adapt to your web framework's request/response API export async function handleWebhook(request: Request) { try { // CRITICAL: Use raw text payload - JSON parsing breaks signature verification const payload = await request.text(); // Use Resend SDK's built-in verification (recommended approach) // Note: header keys are short-form: id, timestamp, signature const event = resend.webhooks.verify({ payload, headers: { id: request.headers.get("svix-id") ?? "", timestamp: request.headers.get("svix-timestamp") ?? "", signature: request.headers.get("svix-signature") ?? "", }, webhookSecret: process.env.RESEND_WEBHOOK_SECRET!, }) as ResendWebhookPayload; // Store event in your database await saveEmailEvent({ emailId: event.data.email_id, type: event.type, recipient: event.data.to[0], subject: event.data.subject, clickedLink: event.data.click?.link, occurredAt: new Date(event.data.created_at), }); return new Response(JSON.stringify({ received: true }), { status: 200 }); } catch (err) { console.error("[Webhook] Verification failed:", err); return new Response( JSON.stringify({ error: "Invalid webhook signature" }), { status: 400 }, ); } } ``` **Why good:** Uses SDK's built-in verification, uses correct `webhookSecret` parameter and short-form header keys (`id`, `timestamp`, `signature`), returns 400 on invalid signatures --- ## Pattern 2: Manual Verification with Svix (Alternative) For environments where Resend SDK isn't suitable, use Svix library directly. ```typescript // api/webhooks/resend/route.ts import { Webhook } from "svix"; const WEBHOOK_SECRET = process.env.RESEND_WEBHOOK_SECRET!; // Adapt to your web framework's request/response API export async function handleWebhook(request: Request) { try { // CRITICAL: Use raw text payload - JSON parsing breaks signature verification const payload = await request.text(); // All three headers are required for Svix verification const headers = { "svix-id": request.headers.get("svix-id") ?? "", "svix-timestamp": request.headers.get("svix-timestamp") ?? "", "svix-signature": request.headers.get("svix-signature") ?? "", }; // Verify using Svix library const wh = new Webhook(WEBHOOK_SECRET); const event = wh.verify(payload, headers); // Process the verified event... return new Response(JSON.stringify({ received: true }), { status: 200 }); } catch (err) { console.error("[Webhook] Verification failed:", err); return new Response( JSON.stringify({ error: "Invalid webhook signature" }), { status: 400 }, ); } } ``` **Why good:** Uses Svix library directly for signature verification, includes all three required headers, handles errors gracefully --- ## Pattern 3: Webhook Configuration Steps to configure webhooks in Resend Dashboard: 1. Go to Resend Dashboard > Webhooks 2. Add endpoint: `https://yourdomain.com/api/webhooks/resend` 3. Select events: sent, delivered, opened, clicked, bounced, complained 4. Copy the signing secret to `RESEND_WEBHOOK_SECRET` --- ## Event Types Reference | Event | Description | | ------------------ | ------------------------------- | | `email.sent` | Email accepted by Resend | | `email.delivered` | Email delivered to recipient | | `email.opened` | Recipient opened the email | | `email.clicked` | Recipient clicked a link | | `email.bounced` | Email bounced (invalid address) | | `email.complained` | Recipient marked as spam | --- ## Security Notes - **Always verify webhook signatures** - prevents spoofed and replay attacks - **Use raw request body** (`request.text()`) - JSON parsing/stringifying breaks signature verification - **SDK method uses `webhookSecret`** parameter (not `secret`) - **SDK header keys are short-form:** `id`, `timestamp`, `signature` (mapped from `svix-id`, `svix-timestamp`, `svix-signature`) - **Svix method uses full header names:** `svix-id`, `svix-timestamp`, `svix-signature` - Store `RESEND_WEBHOOK_SECRET` in environment variables - Return 400 for invalid signatures, 200 for valid events - Prefer `resend.webhooks.verify()` SDK method over manual Svix verification
-
-
reference.md 4.1 KB
# Email Reference > Decision frameworks, anti-patterns, and red flags for the Email skill. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) folder for code examples. --- <decision_framework> ## Decision Framework ### Sync vs Async Sending ``` Is email required for the response? ├── YES (e.g., password reset confirmation) │ └── Await the send, handle errors └── NO (e.g., welcome email, notification) └── Send async, don't await ``` ### Single vs Batch Sending ``` How many emails? ├── 1 email → resend.emails.send() ├── 2-100 emails → resend.batch.send() └── 100+ emails → Loop with batch API ``` ### Retry Strategy ``` Is the error retryable? ├── Rate limit → Retry with exponential backoff ├── Server error (5xx) → Retry with backoff ├── Invalid email → Don't retry, log error ├── Authentication error → Don't retry, check API key └── Quota exceeded → Don't retry, upgrade plan ``` ### Email Category ``` What type of email is this? ├── Transactional (verification, password reset) │ └── Always send, no unsubscribe needed ├── Notification (mentions, comments) │ └── Check preferences, include unsubscribe └── Marketing (promotions, newsletters) └── Require explicit opt-in, include unsubscribe ``` ### Scheduled vs Immediate Sending ``` Should email be sent now? ├── Time-sensitive (password reset, verification) │ └── Send immediately ├── User timezone matters (reminders, digests) │ └── Schedule for appropriate local time ├── Campaign with specific launch time │ └── Schedule using scheduledAt parameter └── Batch notification └── Note: scheduledAt not supported in batch API ``` ### Idempotency Key Usage ``` Should you use an idempotency key? ├── Payment/order confirmations │ └── YES - use order ID as key ├── User-triggered actions (signup, password reset) │ └── YES - use request ID or user+action combo ├── Retryable requests (webhook handlers, queues) │ └── YES - prevents duplicate sends on retry └── One-off manual sends └── Optional - not strictly necessary ``` </decision_framework> --- <anti_patterns> ## Anti-Patterns ### Not Awaiting render() ```typescript // ANTI-PATTERN: Forgetting to await const html = render(WelcomeEmail({ userName })); await resend.emails.send({ html }); // Sends "[object Promise]"! ``` **Why it's wrong:** render() returns a Promise, email body will be garbage. **What to do instead:** Always `const html = await render(...)`. --- ### Client-Side Email Sending ```typescript // ANTI-PATTERN: Exposing API key to client const resend = new Resend(process.env.PUBLIC_RESEND_KEY); // API key visible in browser bundle! ``` **Why it's wrong:** API key exposed, anyone can send emails as you. **What to do instead:** Only send emails from server-side code. --- ### Silent Failure ```typescript // ANTI-PATTERN: No error handling await resend.emails.send({ ... }); // If this fails, no one knows! ``` **Why it's wrong:** Lost emails, confused users, no debugging info. **What to do instead:** Check error response, log failures, implement retry. --- ### Missing Unsubscribe ```typescript // ANTI-PATTERN: No unsubscribe in marketing email const MarketingEmail = () => ( <BaseLayout> <Text>Check out our new features!</Text> {/* No unsubscribe link - illegal! */} </BaseLayout> ); ``` **Why it's wrong:** CAN-SPAM violation, users mark as spam instead. **What to do instead:** Always include unsubscribe link in non-transactional emails. --- ### Ignoring Preferences ```typescript // ANTI-PATTERN: Sending without checking preferences async function sendNewsletter(users: User[]) { for (const user of users) { await sendEmail({ to: user.email, ... }); // User may have opted out! } } ``` **Why it's wrong:** Spam to users who opted out, damages reputation. **What to do instead:** Check email preferences before sending non-transactional emails. </anti_patterns> -
SKILL.md 11.1 KB
--- name: api-email-resend-react-email description: Resend + React Email templates --- # Email Patterns with Resend and React Email > **Quick Guide:** Use Resend for transactional emails with React Email templates. Always `await render()` before sending (it returns a Promise). Server-side only - never expose API keys to clients. Implement retry with exponential backoff for transient failures. Include unsubscribe links in non-transactional emails (CAN-SPAM). Use `resend.batch.send()` for 2-100 recipients (no attachments or scheduling support in batch). React Email 5.0+ deprecated `renderAsync` - use `render()` instead. Webhook verification requires raw request body and `webhookSecret` parameter. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST await `render()` before passing HTML to resend.emails.send() - render returns a Promise)** **(You MUST handle Resend API errors and implement retry logic for transient failures)** **(You MUST use server-side sending for all emails - never expose RESEND_API_KEY to the client)** **(You MUST include unsubscribe links in marketing/notification emails - required for CAN-SPAM compliance)** **(You MUST use typed props interfaces for all email templates - enables compile-time validation)** </critical_requirements> --- **Auto-detection:** Resend, React Email, @react-email/components, resend.emails.send, email template, transactional email, verification email, password reset email, notification email, email rendering, resend.batch.send, resend.webhooks.verify **When to use:** - Sending transactional emails (verification, password reset, receipts) - Creating React Email templates with Tailwind styling - Building notification systems with email delivery - Implementing email tracking via webhooks - Batch sending to multiple recipients **When NOT to use:** - Marketing campaign management (use dedicated marketing tools) - SMS or push notifications (different services) - Email list management (use Resend Audiences or marketing tools) --- <philosophy> ## Philosophy Email in modern applications follows a **server-side, template-driven** approach. React Email brings component patterns to email development, while Resend handles reliable delivery. **Core principles:** 1. **Server-side only** - Never expose API keys to clients 2. **Typed templates** - Props interfaces catch errors at compile time 3. **Reliable delivery** - Error handling with retry logic for transient failures 4. **Non-blocking** - Fire-and-forget for non-critical emails **When to send emails:** - User authentication events (verification, password reset, 2FA) - Transactional confirmations (purchases, signups, invitations) - Important notifications (security alerts, account changes) - Team collaboration (invites, mentions, updates) **When NOT to send emails:** - Every minor action (creates email fatigue) - Marketing without consent (spam, illegal) - Real-time alerts (use push notifications) - In-app actions (show in-app notifications instead) </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Email Template Structure Define typed props, use React Email components, add PreviewProps for the dev server. ```typescript interface WelcomeEmailProps { userName: string; loginUrl: string; features?: string[]; } export function WelcomeEmail({ userName, loginUrl, features = [] }: WelcomeEmailProps) { return ( <BaseLayout preview={`Welcome, ${userName}!`}> <Heading>Welcome!</Heading> <Text>Hi {userName},</Text> <Button href={loginUrl}>Get Started</Button> </BaseLayout> ); } WelcomeEmail.PreviewProps = { userName: "John", loginUrl: "..." } satisfies WelcomeEmailProps; ``` See [examples/core.md](examples/core.md) Pattern 1 for complete template with layout and styling. --- ### Pattern 2: Sending with Error Handling Always await `render()`, check the `{ data, error }` response, return typed results. ```typescript const html = await render(options.react); // CRITICAL: must await const { data, error } = await resend.emails.send({ from: `${DEFAULT_FROM_NAME} <${DEFAULT_FROM_ADDRESS}>`, to: options.to, subject: options.subject, html, }); if (error) { return { success: false, error: error.message }; } return { success: true, id: data?.id }; ``` See [examples/core.md](examples/core.md) Pattern 2-3 for complete send wrapper and retry with exponential backoff. --- ### Pattern 3: Async (Fire-and-Forget) Sending For non-critical emails (welcome, notifications), don't block the response. ```typescript // Non-blocking - catches errors internally sendEmailAsync(options); return Response.json({ success: true }); // Returns immediately ``` Track in-flight promises for graceful shutdown. See [examples/async-batch.md](examples/async-batch.md) Pattern 1-2. --- ### Pattern 4: Batch API Use `resend.batch.send()` for 2-100 recipients. Render all templates in parallel. ```typescript const rendered = await Promise.all( emails.map(async (e) => ({ from, to: e.to, subject: e.subject, html: await render(e.react), })), ); const { data, error } = await resend.batch.send(rendered); ``` **Batch limitations:** No `attachments`, no `scheduledAt`. Tags and idempotency keys are supported. See [examples/async-batch.md](examples/async-batch.md) Pattern 3-4. --- ### Pattern 5: Webhook Verification Use `resend.webhooks.verify()` with the raw request body. JSON parsing breaks signature verification. ```typescript const payload = await request.text(); // Raw body, NOT .json() const event = resend.webhooks.verify({ payload, headers: { id: request.headers.get("svix-id") ?? "", timestamp: request.headers.get("svix-timestamp") ?? "", signature: request.headers.get("svix-signature") ?? "", }, webhookSecret: process.env.RESEND_WEBHOOK_SECRET!, }); ``` See [examples/webhooks.md](examples/webhooks.md) for full handler with event processing and Svix alternative. --- ### Pattern 6: Scheduled Sending, Idempotency Keys, Tags ```typescript // Scheduled (up to 30 days, NOT supported in batch) await resend.emails.send({ ...payload, scheduledAt: futureDate.toISOString() }); // Idempotency (256 char limit, expires 24h) — second argument to send() await resend.emails.send(payload, { idempotencyKey: `order-confirmation-${orderId}`, }); // Tags (ASCII alphanumeric, underscores, dashes only) await resend.emails.send({ ...payload, tags: [{ name: "campaign", value: "launch" }], }); ``` See [examples/advanced-features.md](examples/advanced-features.md) for complete implementations with validation. --- ### Pattern 7: Unsubscribe and Preferences Non-transactional emails MUST include unsubscribe links (CAN-SPAM). Use signed tokens for security. ```typescript // In every notification/marketing template: <Link href={unsubscribeUrl}>Unsubscribe from these notifications</Link> // Generate signed unsubscribe URLs const token = jwt.sign({ userId, category }, UNSUBSCRIBE_SECRET, { expiresIn: "30d" }); const url = `${APP_URL}/api/email/unsubscribe?token=${token}`; ``` See [examples/preferences.md](examples/preferences.md) for preference schema, checking before send, and unsubscribe endpoint. </patterns> --- **Detailed Resources:** - [examples/core.md](examples/core.md) - Template structure, sending with error handling, retry logic - [examples/async-batch.md](examples/async-batch.md) - Async sending, batch API - [examples/webhooks.md](examples/webhooks.md) - Webhook handler with signature verification - [examples/templates.md](examples/templates.md) - Password Reset, Notification templates - [examples/preferences.md](examples/preferences.md) - Unsubscribe, email preferences - [examples/advanced-features.md](examples/advanced-features.md) - Scheduled sending, idempotency keys, tags - [reference.md](reference.md) - Decision frameworks, anti-patterns --- <red_flags> ## RED FLAGS **High Priority Issues:** - Not awaiting `render()` - sends `"[object Promise]"` as email body - API key exposed on client - security vulnerability - No error handling - silent failures - Missing unsubscribe links in non-transactional emails - CAN-SPAM violation - Sending without checking user preferences - spam **Medium Priority Issues:** - No retry logic for transient failures (rate limits, 5xx errors) - Sync sending blocking request handlers for non-critical emails - Hardcoded from address instead of environment variable - No webhook verification signature check - Not logging email send results **Common Mistakes:** - Using `Grid` or `Flexbox` in email templates (not supported by email clients) - Expecting shadows or gradients to render in emails - Using `rem` units (email clients handle differently) - Forgetting `PreviewProps` for dev server **Gotchas & Edge Cases:** - Resend SDK accepts a `react` prop directly (renders internally), but pre-rendering with `await render()` + `html` gives you control over the output and works outside the Resend SDK - `render()` is async in React Email 5.0+ (`renderAsync` deprecated) - Batch API limited to 100 emails, does NOT support `attachments` or `scheduledAt` - Webhooks require raw request body - JSON parsing breaks signature verification - Webhook verify uses `webhookSecret` parameter (not `secret`) - Webhook headers object uses short keys: `id`, `timestamp`, `signature` - Idempotency keys are passed as a second argument to `resend.emails.send()`, not in the email payload headers - Idempotency keys expire after 24 hours, max 256 characters - Tags: ASCII alphanumeric, underscores, dashes only, max 256 chars per key/value - Tailwind in emails requires `@react-email/tailwind` wrapper (Tailwind 4 supported in React Email 5.0+) - Images must use absolute URLs (no relative paths) </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST await `render()` before passing HTML to resend.emails.send() - render returns a Promise)** **(You MUST handle Resend API errors and implement retry logic for transient failures)** **(You MUST use server-side sending for all emails - never expose RESEND_API_KEY to the client)** **(You MUST include unsubscribe links in marketing/notification emails - required for CAN-SPAM compliance)** **(You MUST use typed props interfaces for all email templates - enables compile-time validation)** **Failure to follow these rules will cause email delivery failures, security vulnerabilities, or legal compliance issues.** </critical_reminders> --- ## Sources - [Resend Node.js SDK](https://resend.com/docs/send-with-nodejs) - [Resend Send Email API](https://resend.com/docs/api-reference/emails/send-email) - [Resend Batch API](https://resend.com/docs/api-reference/emails/send-batch-emails) - [Resend Webhooks Verification](https://resend.com/docs/dashboard/webhooks/verify-webhooks-requests) - [Resend Idempotency Keys](https://resend.com/blog/engineering-idempotency-keys) - [Resend Error Handling](https://resend.com/docs/api-reference/errors) - [React Email Components](https://react.email/docs/components) - [React Email 5.0 Release](https://resend.com/blog/react-email-5)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.