payload-email-cloudflare
v1.0.1
Published
Payload Cloudflare Email Sending Adapter (Workers send_email binding)
Maintainers
Readme
Cloudflare Email Adapter
This adapter lets Payload send email through the
Cloudflare Email Sending
Workers binding (send_email).
Because it uses the binding directly rather than a REST API, no API token is
required — but your Payload app must run on Cloudflare Workers (e.g. via
OpenNext) so the binding is available on
env.
Installation
pnpm add payload-email-cloudflarePrerequisites
Onboard your sending domain. The
fromaddress must use a domain that has been enabled for Email Sending:npx wrangler email sending enable yourdomain.comVerify it's listed with
npx wrangler email sending list.Add the
send_emailbinding to yourwrangler.jsonc:{ "send_email": [{ "name": "EMAIL" }] }For local development, add
"remote": trueso sends are proxied to the real service:{ "send_email": [{ "name": "EMAIL", "remote": true }] }Run
npx wrangler typesto generate theEnvtype forenv.EMAIL.
Usage
The adapter needs the binding object (env.EMAIL). On Cloudflare, retrieve it
from the request context and pass it to cloudflareAdapter.
// payload.config.ts
import { getCloudflareContext } from '@opennextjs/cloudflare'
import { cloudflareAdapter } from 'payload-email-cloudflare'
import { buildConfig } from 'payload'
const { env } = getCloudflareContext()
export default buildConfig({
email: cloudflareAdapter({
binding: env.EMAIL,
defaultFromAddress: '[email protected]',
defaultFromName: 'Payload CMS',
}),
// ...rest of your config
})binding is typed as SendEmail from
@cloudflare/workers-types,
so env.EMAIL (after wrangler types) is assignable directly — no cast needed.
Sending email
Once the adapter is configured, send mail through Payload's sendEmail from
anywhere you have access to the payload instance — a hook, a custom endpoint,
a job, etc. The adapter maps the message to the send_email binding for you.
await payload.sendEmail({
to: '[email protected]',
subject: 'Welcome aboard',
html: '<h1>Welcome!</h1><p>Thanks for signing up.</p>',
text: 'Welcome! Thanks for signing up.',
})from is optional — when omitted, defaultFromAddress / defaultFromName are
used. You can also set cc, bcc, replyTo, attachments, and headers:
await payload.sendEmail({
from: { address: '[email protected]', name: 'Sales' },
to: ['[email protected]', '[email protected]'],
cc: '[email protected]',
replyTo: '[email protected]',
subject: 'Your invoice',
html: '<p>See the attached invoice.</p>',
attachments: [
{
filename: 'invoice.pdf',
content: pdfBuffer, // string | Buffer | ArrayBuffer (see Attachments)
contentType: 'application/pdf',
},
],
})A common place to call it is from a collection hook — for example, emailing a user after they're created:
// collections/Users.ts
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
hooks: {
afterChange: [
async ({ doc, operation, req }) => {
if (operation === 'create') {
await req.payload.sendEmail({
to: doc.email,
subject: 'Welcome aboard',
html: `<p>Welcome, ${doc.email}!</p>`,
})
}
},
],
},
fields: [],
}On success the binding resolves with a messageId; on failure the adapter throws
a Payload APIError — see Error handling.
Options
| Option | Type | Required | Description |
| -------------------------- | ------------ | -------- | ------------------------------------------------------------- |
| binding | SendEmail | Yes | The send_email binding, e.g. env.EMAIL. |
| defaultFromAddress | string | Yes | Fallback from address when an email doesn't specify one. |
| defaultFromName | string | Yes | Fallback from display name. |
| overrideRecipientAddress | string | No | Send every email to this address instead. Useful for testing. |
Attachments
The Workers binding sends raw bytes, so attachments must provide content as a
string, Buffer, or ArrayBuffer. Path-based attachments ({ path }) are not
supported — there is no filesystem to read from in a Worker. Inline images
(cid) are mapped to disposition: 'inline' and referenced in HTML as
cid:<cid>. Total email size (body + attachments) cannot exceed 25 MiB.
Error handling
The binding throws on failure. This adapter catches the error and re-throws a
Payload APIError whose message includes the Cloudflare E_* error code (e.g.
E_SENDER_NOT_VERIFIED, E_RATE_LIMIT_EXCEEDED) and maps it to an appropriate
HTTP status. See the
error code reference for the
full list.
