@cfast/email
Overview
Section titled “Overview”Cloudflare Workers cannot use SMTP, and most email libraries assume Node.js. @cfast/email is a Workers-native email client that renders templates with react-email and sends them through a pluggable provider backend. It ships with a Mailgun provider for production and a console provider for local development.
Templates are plain React components. Write them with @react-email/components for structured layouts, or use plain JSX. The client renders them to HTML and plain text at send time, then delegates delivery to whichever provider you have configured.
Installation
Section titled “Installation”pnpm add @cfast/email @react-email/componentsNo peer dependencies beyond React.
Quick Setup
Section titled “Quick Setup”Create an email client with a provider and default sender:
import { createEmailClient } from "@cfast/email";import { mailgun } from "@cfast/email/mailgun";import { env } from "~/env";
export const email = createEmailClient({ provider: mailgun(() => ({ apiKey: env.get().MAILGUN_API_KEY, domain: env.get().MAILGUN_DOMAIN, })), from: () => `MyApp <noreply@${env.get().MAILGUN_DOMAIN}>`,});Both provider config and from use getter functions — they are called lazily at send time, which is the Workers-friendly pattern for accessing environment bindings.
Then send an email with a React component as the body:
import { email } from "~/email.server";import { WelcomeEmail } from "~/email/templates/welcome";
await email.send({ to: "user@example.com", subject: "Welcome to MyApp", react: <WelcomeEmail name="Daniel" />,});Core Concepts
Section titled “Core Concepts”React Email Templates
Section titled “React Email Templates”Templates are standard React components. You can use @react-email/components for cross-client email structure, or write plain JSX:
import { Html, Head, Body, Text, Link } from "@react-email/components";
type WelcomeEmailProps = { name: string;};
export function WelcomeEmail({ name }: WelcomeEmailProps) { return ( <Html> <Head /> <Body> <Text>Hi {name},</Text> <Text>Welcome to MyApp.</Text> <Link href="https://myapp.com/dashboard">Go to Dashboard</Link> </Body> </Html> );}The client renders each component to both HTML and plain text via @react-email/render, so every email has a text fallback automatically.
Pluggable Providers
Section titled “Pluggable Providers”The email client is decoupled from the delivery mechanism. A provider is a plain object with a name and a send method. Swap providers without changing any template or send-site code.
Lazy Configuration
Section titled “Lazy Configuration”Provider config and the from address use getter functions instead of static values. This is important in Workers, where environment bindings are only available during request handling — not at module evaluation time.
Common Patterns
Section titled “Common Patterns”Development with the Console Provider
Section titled “Development with the Console Provider”During local development, use the console provider to log emails instead of sending them:
import { console as consoleDev } from "@cfast/email/console";
export const email = createEmailClient({ provider: consoleDev(), from: () => "Dev <dev@localhost>",});This logs the recipient, subject, and HTML length to the console with a generated message ID. No external service needed.
Error Handling
Section titled “Error Handling”Providers throw EmailDeliveryError on delivery failures, with structured metadata about what went wrong:
import { EmailDeliveryError } from "@cfast/email";
try { await email.send({ to, subject, react: <Template /> });} catch (error) { if (error instanceof EmailDeliveryError) { console.error(error.provider); // "mailgun" console.error(error.statusCode); // 401 console.error(error.response); // "Unauthorized" }}The client does not catch internally — callers decide whether to fire-and-forget or handle errors.
Building a Custom Provider
Section titled “Building a Custom Provider”A provider implements the EmailProvider interface. Use fetch to call your provider’s HTTP API — no SMTP or Node.js libraries needed:
import type { EmailProvider, EmailMessage } from "@cfast/email";import { EmailDeliveryError } from "@cfast/email";
const resend: EmailProvider = { name: "resend",
async send(message: EmailMessage): Promise<{ id: string }> { const response = await fetch("https://api.resend.com/emails", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ to: message.to, from: message.from, subject: message.subject, html: message.html, text: message.text, }), });
if (!response.ok) { throw new EmailDeliveryError(await response.text(), { provider: "resend", statusCode: response.status, response: await response.text(), }); }
const data = await response.json(); return { id: data.id }; },};