Skip to content

feat: Add Resend as a transactional email provider #42

Description

@babblebey

Add support for Resend as a transactional email provider, alongside the existing ZeptoMail integration. Resend exposes a simple REST API for sending email and is a popular choice for developer-oriented apps.

Background

The email layer is already provider-agnostic behind a TransactionalEmailProvider interface (see src/server/email/). ZeptoMail is currently the only concrete provider. Resend should slot in as a second implementation following the same patterns.

Key files:

Tasks

1. Update src/server/email/types.ts

Extend EmailDriver to include "resend":

// Before
export type EmailDriver = "zeptomail";

// After
export type EmailDriver = "zeptomail" | "resend";

Also update SendTransactionalEmailResult.driver — it already uses EmailDriver, so this should propagate automatically.

2. Add ResendEmailProvider

Create src/server/email/resend-provider.ts implementing the TransactionalEmailProvider interface, modelled after zeptomail-provider.ts:

export class ResendEmailProvider implements TransactionalEmailProvider {
  readonly driver = "resend" as const;

  private readonly apiKey: string;

  constructor() {
    this.apiKey = env.RESEND_API_KEY
      ? String(env.RESEND_API_KEY)
      : missingConfig("RESEND_API_KEY");
  }

  async send(message: TransactionalEmailMessage): Promise<SendTransactionalEmailResult> {
    // POST https://api.resend.com/emails
    // Authorization: Bearer <RESEND_API_KEY>
    // Body: { from, to, subject, html, text }
  }
}

The Resend API endpoint is https://api.resend.com/emails. Authentication uses a Bearer token. The request body maps cleanly from TransactionalEmailMessage:

Resend field Source
from message.from.email (with optional name as "Name <email>")
to [message.to.email]
subject message.subject
html message.html
text message.text

Include:

  • Structured console.info / console.error logging matching the [tx-email:resend:*] pattern from the ZeptoMail implementation.
  • Error mapping for 401/403 → authentication, 429 → rate-limit, 5xx → provider-unavailable, other 4xx → invalid-request.
  • missingConfig() helper for required env var validation.

3. Update src/server/email/provider.ts

Wire ResendEmailProvider into the factory and driver resolver:

// createEmailProvider()
case "resend":
  return new ResendEmailProvider();

// getConfiguredEmailDriver()
case "resend":
  return "resend";

Update the error message to list resend as a valid option:

`Unsupported EMAIL_DRIVER value: ${rawDriver}. Expected one of: zeptomail, resend`

4. Update src/env.js

  • Add "resend" to the EMAIL_DRIVER enum:
    EMAIL_DRIVER: z.enum(["zeptomail", "resend"]).optional(),
  • Add a isResendDriver flag (mirroring isZeptoMailDriver).
  • Add RESEND_API_KEY with a conditional .refine() that requires it when EMAIL_DRIVER=resend:
    RESEND_API_KEY: z.string().optional().refine(
      (value) => !isResendDriver || Boolean(value),
      { message: "RESEND_API_KEY is required when EMAIL_DRIVER=resend" }
    ),
  • Add RESEND_API_KEY to runtimeEnv.

5. Update src/server/api/routers/setup.ts

Add a probeResendCredentials() function (analogous to probeZeptoMailCredentials()) that makes a lightweight request to https://api.resend.com/emails with a Bearer token to verify credentials, and wire it into the email health check block.

6. Export from src/server/email/index.ts

Export ResendEmailProvider from src/server/email/index.ts (or let it be picked up via the existing export * from "./provider" if only exposed through the factory).

7. Add tests

Add test/server/email/resend-provider.test.ts covering:

  • Successful send maps response to SendTransactionalEmailResult correctly.
  • 401/403 throws TransactionalEmailError with code: "authentication".
  • 429 throws with code: "rate-limit" and retryable: true.
  • 5xx throws with code: "provider-unavailable" and retryable: true.
  • Network failure throws with code: "provider-unavailable".
  • Missing RESEND_API_KEY throws with code: "configuration".

Update test/server/email/provider.test.ts to cover:

  • createEmailProvider() returns a ResendEmailProvider when EMAIL_DRIVER=resend.
  • getConfiguredEmailDriver() returns "resend" for that value.

8. Update docs

  • Update README.md to document EMAIL_DRIVER=resend and RESEND_API_KEY.
  • Update any other relevant docs (e.g. self-hosted setup guide) to list Resend as a supported email provider option alongside ZeptoMail.

Acceptance Criteria

  • ResendEmailProvider implements the full TransactionalEmailProvider interface.
  • EMAIL_DRIVER=resend + RESEND_API_KEY fully replaces ZeptoMail for invite and claim email delivery.
  • Setup health check correctly probes and reports Resend credential validity.
  • Existing ZeptoMail behaviour is completely unaffected.
  • Tests pass for the new provider and updated factory.
  • Docs are updated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions