Beignet is experimental alpha software. The 0.0.x package line is for early
evaluation, and APIs may change between releases while the framework settles.
Resend-backed mail provider for Beignet.
The provider installs the app-facing ctx.ports.mailer port and exposes
ctx.ports.resend.client only as an escape hatch for Resend-specific features.
createResendMailProvider(...) returns the stable ResendMailProvider type.
ResendMailConfig describes its validated config; the Zod schema remains
internal.
bun add @beignet/provider-mail-resend @beignet/core resend
The provider supports resend ^2, ^3, ^4, and ^6. Resend v6 inherits
the SDK's Node.js 20+ runtime requirement.
import { createResendMailProvider } from "@beignet/provider-mail-resend";
import { createServer } from "@beignet/core/server";
const server = await createServer({
ports: basePorts,
providers: [createResendMailProvider()],
context: ({ ports }) => ({ ports }),
routes,
});
Required environment variables:
| Variable | Description |
|---|---|
RESEND_API_KEY |
Resend API key |
RESEND_FROM |
Default sender, e.g. My App <no-reply@example.com> |
beignet doctor --strict checks that installed Resend mail providers are
registered in server/providers.ts and that both required env vars are present
in app env examples or config.
Use createResendMailProvider(...) when you want to pass config directly
instead of reading RESEND_* env vars. Options override env-derived values:
import { createResendMailProvider } from "@beignet/provider-mail-resend";
export const providers = [
createResendMailProvider({
apiKey: secrets.resendApiKey,
from: "My App <no-reply@example.com>",
}),
];
Calling createResendMailProvider() with no options uses the env-backed
configuration.
await ctx.ports.mailer.send({
to: "user@example.com",
subject: "Welcome",
html: "<h1>Hello</h1>",
});
The same MailerPort works with SMTP, memory fakes, and other adapters:
await ctx.ports.mailer.send({
from: { email: "support@example.com", name: "Support" },
to: ["user@example.com", "admin@example.com"],
replyTo: "support@example.com",
subject: "Account updated",
text: "Your account was updated.",
html: "<p>Your account was updated.</p>",
});
Use the Resend client only when you need a Resend-specific feature not covered
by MailerPort:
await ctx.ports.resend.client.emails.send({
from: "sender@example.com",
to: "user@example.com",
subject: "Invoice",
html: "<p>Attached.</p>",
attachments: [
{
filename: "invoice.pdf",
content: pdfBuffer,
},
],
});
The provider contributes:
ctx.ports.mailer, the standard Beignet MailerPortctx.ports.resend, an escape hatch with the Resend client and default senderWhen ctx.ports.devtools is installed, this provider records mail.send,
mail.sent, and mail.failed events under the mail watcher. Completed and
failed events include durationMs in their details.
The provider fails fast and does not retry. Each send(...) call makes one
Resend SDK call, even for transient network errors or provider-side failures.
Mail delivery is not idempotent: a timed-out send may still have been
delivered, so a blind provider retry risks duplicate emails.
Delivery failures throw MailDeliveryError from @beignet/core/mail with the
provider name, the recipient count, and the original Resend error preserved as
cause. Message bodies and credentials are never included. Startup
configuration problems throw during provider setup.
When mail needs retries, dispatch it from a job or an outbox-backed listener and own idempotency there. The job or outbox row should choose attempts and delay, and the application should record one delivery per business event before sending.
Use a memory or fake MailerPort in use-case tests. Keep Resend sandbox or
test-mode credentials in environment-specific config and avoid sending real
mail from automated tests.
Set RESEND_FROM to a verified domain sender. Treat mail delivery as an
external side effect: trigger important mail from committed workflow state,
usually through notifications, jobs, or outbox-backed listeners.
MIT