Emails

Send emails with React Email and a provider-agnostic email worker. Switch between Resend, SES, Mailgun, SparkPost, or custom SMTP.

Nuxfire ships with an email system that runs entirely on Cloudflare Workers, built on a vendored fork of AdonisJS Mail adapted to run without the AdonisJS framework — @nuxfire/emails/mailer (packages/emails/mailer), owned source rather than a dependency on a third-party fork.

Architecture

  • Email Templates (packages/emails/templates) — type-safe templates using React Email
  • Mailer Engine (packages/emails/mailer) — the provider-agnostic send engine, a fork of AdonisJS Mail
  • Email Worker (apps/functions/src/emails.ts) — a Cloudflare Worker Entrypoint that handles provider-agnostic sending
  • Infrastructure (infra/emails.ts) — infra config for the Worker

Email Templates

Templates are built with React Email and Tailwind. For example:

packages/emails/templates/Welcome.tsx
import { Body, Container, Html, Text } from "@react-email/components";

export const WelcomeEmail = ({ username }: { username: string }) => (
  <Html>
    <Body>
      <Tailwind>
        <Container class="p-4 border border-solid border-[#eaeaea] rounded my-[40px] mx-auto max-w-[465px]">
          <Text>Welcome {username}!</Text>
        </Container>
      </Tailwind>
    </Body>
  </Html>
);

Every real template also receives the installation's brand (name and logoUrl) as a second argument, built from your Branding — so subject lines and the logo follow your BRAND_NAME and BRAND_DOMAIN with nothing to edit in the templates.

Provider Configuration

Who the email comes from is not configured here, and callers never pass it: the sender is your BRAND_EMAIL (shown as BRAND_NAME), set in the Branding section of the stage's env file and handed to the emails Worker at deploy time (infra/emails.ts). The domain of that address must be verified in your email provider.

Email providers are enabled in config.ts's flags, and wired up in the emails Worker itself. Only Resend is uncommented by default — the other transports are ready to enable, not deleted, with the exact import/config lines to uncomment marked TODO:

config.ts
export const flags: {
  // ...
  resend?: boolean;
  mailgun?: boolean;
  ses?: boolean;
  sparkPost?: boolean;
  smtp?: boolean;
} = {
  // ...
  resend: true,
};
apps/functions/src/emails.ts
// TODO: import and set up email providers that you want to use
import { ResendTransport } from "@nuxfire/emails/mailer/transports/resend";
// import { MailgunTransport } from "@nuxfire/emails/mailer/transports/mailgun";
// ...

const config: Parameters<typeof generateMailer>[0] = {
  default: "resend",
  mailers: {
    ...(flags.resend && {
      resend: () =>
        new ResendTransport({
          key: Resource.RESEND_API_KEY.value || "",
          baseUrl: "https://api.resend.com",
        }),
    }),
    // ...(flags.mailgun && { mailgun: () => new MailgunTransport({ ... }) }),
  },
};

Resource.RESEND_API_KEY matches the secret name exactly (RESEND_API_KEY in .env.example/infra/secret.ts) — every provider secret follows this same SCREAMING_SNAKE_CASE naming, resolved through SST's own sst/resource binding rather than process.env.

Setting Up Secrets

# Resend
sst secret set RESEND_API_KEY re_123...
# Mailgun
sst secret set MAILGUN_API_KEY key-123...

Sending Emails

Use the sendEmail function from the tRPC context to send emails:

async ({ ctx: { user, origin, authorize, sendEmail }, input }) => {
  // ...
  await sendEmail(invite.email, "Invitation", {
    teamName: team.name,
    invitedBy: user.name,
    inviteLink,
  });
  // ...
};

The emails Worker can also be invoked directly from any package via the sender helper:

import { sender } from "@nuxfire/functions/sender";
const sendEmail = sender(event.context?.cloudflare?.env?.Emails); // pass the Emails binding
Nuxfire Production Kit

Ready to build and launch your SaaS?

Get 100% full source code ownership, zero proprietary wrappers, and architecture engineered for millions of requests on Cloudflare.

© 2026 Nuxfire