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
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
TransactionalEmailProviderinterface (seesrc/server/email/). ZeptoMail is currently the only concrete provider. Resend should slot in as a second implementation following the same patterns.Key files:
src/server/email/types.ts—EmailDriver,TransactionalEmailProviderinterface, shared typessrc/server/email/zeptomail-provider.ts— reference implementation to mirrorsrc/server/email/provider.ts— factory/singleton with exhaustive driver switchsrc/env.js— env schema with ZeptoMail-conditional validationsrc/server/api/routers/setup.ts— setup health check that probes email credentialsTasks
1. Update
src/server/email/types.tsExtend
EmailDriverto include"resend":Also update
SendTransactionalEmailResult.driver— it already usesEmailDriver, so this should propagate automatically.2. Add
ResendEmailProviderCreate
src/server/email/resend-provider.tsimplementing theTransactionalEmailProviderinterface, modelled afterzeptomail-provider.ts:The Resend API endpoint is
https://api.resend.com/emails. Authentication uses aBearertoken. The request body maps cleanly fromTransactionalEmailMessage:frommessage.from.email(with optional name as"Name <email>")to[message.to.email]subjectmessage.subjecthtmlmessage.htmltextmessage.textInclude:
console.info/console.errorlogging matching the[tx-email:resend:*]pattern from the ZeptoMail implementation.authentication, 429 →rate-limit, 5xx →provider-unavailable, other 4xx →invalid-request.missingConfig()helper for required env var validation.3. Update
src/server/email/provider.tsWire
ResendEmailProviderinto the factory and driver resolver:Update the error message to list
resendas a valid option:`Unsupported EMAIL_DRIVER value: ${rawDriver}. Expected one of: zeptomail, resend`4. Update
src/env.js"resend"to theEMAIL_DRIVERenum:isResendDriverflag (mirroringisZeptoMailDriver).RESEND_API_KEYwith a conditional.refine()that requires it whenEMAIL_DRIVER=resend:RESEND_API_KEYtoruntimeEnv.5. Update
src/server/api/routers/setup.tsAdd a
probeResendCredentials()function (analogous toprobeZeptoMailCredentials()) that makes a lightweight request tohttps://api.resend.com/emailswith aBearertoken to verify credentials, and wire it into the email health check block.6. Export from
src/server/email/index.tsExport
ResendEmailProviderfromsrc/server/email/index.ts(or let it be picked up via the existingexport * from "./provider"if only exposed through the factory).7. Add tests
Add
test/server/email/resend-provider.test.tscovering:SendTransactionalEmailResultcorrectly.TransactionalEmailErrorwithcode: "authentication".code: "rate-limit"andretryable: true.code: "provider-unavailable"andretryable: true.code: "provider-unavailable".RESEND_API_KEYthrows withcode: "configuration".Update
test/server/email/provider.test.tsto cover:createEmailProvider()returns aResendEmailProviderwhenEMAIL_DRIVER=resend.getConfiguredEmailDriver()returns"resend"for that value.8. Update docs
README.mdto documentEMAIL_DRIVER=resendandRESEND_API_KEY.Acceptance Criteria
ResendEmailProviderimplements the fullTransactionalEmailProviderinterface.EMAIL_DRIVER=resend+RESEND_API_KEYfully replaces ZeptoMail for invite and claim email delivery.