VK sends mail through the provider abstraction in @vergekit/core/email. The main entry point is sendEmail(runtimeEnv, input), which resolves the configured provider from runtimeEnv.EMAIL_PROVIDER and sends one message with the common SendEmailInput shape. App-specific React Email templates live in src/email.

Direct Sends

Use sendEmail from Worker or Astro server code when a route needs to send a custom transactional email.

import { env } from 'cloudflare:workers';
import { sendEmail } from '@vergekit/core/email';

const result = await sendEmail(env, {
  to: { email: '[email protected]', name: 'Customer Name' },
  from: { email: '[email protected]', name: 'VK' },
  subject: 'Your VK receipt',
  html: '<p>Thanks for your order.</p>',
  text: 'Thanks for your order.',
  replyTo: '[email protected]',
});

console.info('sent email', result.provider, result.id);

to can be a single email address, a named address object, or an array of either form. from and replyTo accept the same address forms.

Always include both html and text. The abstraction requires both so every message has a plain-text fallback.

sendEmail does not automatically read EMAIL_FROM for direct sends. Pass from in the message input. EMAIL_FROM is used by the auth-email helper described below.

Provider Configuration

The provider is selected by EMAIL_PROVIDER. When it is missing, the abstraction uses console.

  • console: Requires no config. Logs the email payload and returns { provider: 'console', id: 'console' }. This is the local default.
  • cloudflare: Requires the EMAIL binding. Uses the Worker send_email binding configured in wrangler.jsonc.
  • resend: Requires RESEND_API_KEY. Sends with the Resend HTTP API.
  • mailgun: Requires MAILGUN_API_KEY and MAILGUN_DOMAIN. Sends with the Mailgun HTTP API.

SMTP/Nodemailer adapters are intentionally not part of this Worker runtime surface. Prefer the Cloudflare Email binding for deployed Workers, or a fetch-based provider when an external email service is required.

Put shared, non-secret configuration in wrangler.jsonc:

{
  "vars": {
    "EMAIL_PROVIDER": "resend",
    "EMAIL_FROM": "VK <[email protected]>",
    "EMAIL_REPLY_TO": "[email protected]",
    "MAILGUN_DOMAIN": "mg.example.com",
  },
}

Put local secrets in .dev.vars:

RESEND_API_KEY=your-local-resend-key
MAILGUN_API_KEY=your-local-mailgun-key

For deployed Workers, set provider secrets with Wrangler:

npx wrangler secret put RESEND_API_KEY
npx wrangler secret put MAILGUN_API_KEY

Only configure the secret for the provider the environment uses.

Auth Emails

Verification and password-reset emails use the higher-level auth helper:

import { env } from 'cloudflare:workers';
import { createAuthEmailSenderFromEnv } from '@vergekit/core/email';
import { authEmailOptions } from '@/config/auth-email';

const authEmail = createAuthEmailSenderFromEnv(env, authEmailOptions);

await authEmail.sendVerificationEmail({
  to: '[email protected]',
  name: 'Customer Name',
  url: 'https://example.com/auth/verify?token=...',
});

createAuthEmailSenderFromEnv resolves the mailer and sender from runtime env. authEmailOptions lives in src/config/auth-email.ts and supplies the app's auth email defaults. With EMAIL_PROVIDER=console, it falls back to [email protected] and the app name from src/config/app.ts.

Customize transactional auth email behavior in src/config/auth-email.ts. The email templates stay in src/email/auth/verify-email.tsx and src/email/auth/reset-password.tsx. Preview them with the React Email CLI:

npm run email

The debug email route at src/pages/api/debug/email.ts returns a non-sending response by default. To test delivery manually, enable the commented implementation in that file; it renders src/email/demo.tsx and sends it through the configured provider to verify template rendering and delivery together.

Use this helper for Better Auth verification and reset flows. Use sendEmail directly for other transactional messages.

Error Handling

sendEmail rejects when required provider configuration is missing or when the selected provider returns an error. Callers should catch errors at the route or job boundary and avoid exposing provider response bodies to users.

try {
  await sendEmail(env, message);
} catch (error) {
  console.error('Email send failed', error);
}

The returned id is optional because provider responses differ. Store it only as diagnostic metadata.

Tests

Use the console provider for tests that only need to assert that a send was requested:

const info = vi.fn();

const result = await sendEmail(
  { EMAIL_PROVIDER: 'console' },
  {
    to: '[email protected]',
    from: '[email protected]',
    subject: 'Test email',
    html: '<p>Hello</p>',
    text: 'Hello',
  },
  { console: { info } },
);

expect(result).toEqual({ provider: 'console', id: 'console' });
expect(info).toHaveBeenCalledWith('[email:console]', expect.any(Object));

For Resend or Mailgun provider tests, pass options.fetcher to stub HTTP requests without calling the real provider.