Skip to content

@cfast/email

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.

Terminal window
pnpm add @cfast/email @react-email/components

No peer dependencies beyond React.

Create an email client with a provider and default sender:

app/email.server.ts
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" />,
});

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.

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.

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.

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.

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.

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 };
},
};