Cloudflare Email setup
Cloudflare email
From a fresh Cloudflare account to sending mail via Cloudflare Email Service. One-time setup; later rotations are step 4 only.
Paid
Workers plan needed
Zero
DNS hand-edits
Bearer
auth header
For why Cloudflare is the default, see Cloudflare Email Service.
Prerequisites
Section titled “Prerequisites”- A Cloudflare account that owns (or proxies) the domain you’ll send from.
- Admin access to that account.
- A real email address for the API token’s audit trail.
How the pieces fit
Section titled “How the pieces fit”flowchart LR paid["Workers Paid plan"] domain["Email Service enabled<br/>on your domain"] dns["SPF / DKIM / DMARC<br/>auto-provisioned"] acct["Account ID<br/>scope for the endpoint"] token["API Token<br/>Email Sending: Edit"] api["apps/api<br/>EMAIL_PROVIDER=cloudflare"] paid --> domain --> dns domain --> acct --> api domain --> token --> api
Cloudflare Email setup chain: a Workers Paid plan unlocks the Email Service;
enabling it on your domain auto-provisions SPF, DKIM, and DMARC; from there
you capture the Account ID (used in the endpoint URL) and a scoped API token
(used in the bearer header). The apps/api reads both via
EMAIL_PROVIDER=cloudflare.
You need both the account ID (in the endpoint URL) and the API token (in the bearer header). Lose either and sends fail.
-
Enable Workers Paid. Cloudflare dashboard → Workers & Pages → Plans → upgrade to Workers Paid. Email Service is bundled into this plan; there’s no separate billing line. (Current pricing.)
-
Enable Email Service on your domain. Dashboard → Email → Email Routing or Email Sending → enable for the domain. Cloudflare auto-provisions the required DNS records (SPF, DKIM, DMARC) because the zone is on Cloudflare. That removes the usual hand-edited DNS step, where most transactional-email setups go wrong. Wait for the dashboard to show all three records as Active (usually under a minute).
-
Capture the Account ID. Dashboard → any domain → right sidebar → “Account ID”. 32 hex characters. Drop it into
compose/.env:Configure Account IDinject the account ID into compose/.env $ echo 'CLOUDFLARE_ACCOUNT_ID=your_32_hex_account_id' >> compose/.env -
Scope an API token. Dashboard → My Profile → API Tokens → Create Token → Custom Token with:
- Permissions:
Email Sending: Editonly, scoped to this account. - Account resources: include the specific account.
- TTL: indefinite for now; rotate quarterly or after any staff change.
Copy the token (you can’t view it again). Drop it into
compose/.env:Configure API Tokeninject the scoped API token into compose/.env $ echo 'CLOUDFLARE_EMAIL_API_TOKEN=your_scoped_api_token' >> compose/.env - Permissions:
-
Set the sender + provider.
Configure Outbound Sender & Providerconfigure the email provider and verified sender domain $ echo 'EMAIL_PROVIDER=cloudflare' >> compose/.env $ echo 'EMAIL_FROM=noreply@yourdomain.com' >> compose/.envEMAIL_FROMmust be on a domain you’ve enabled Email Service for. Sending from a domain that isn’t enabled returns a 403. -
Smoke-test.
Smoke-Test Outbound Deliveryrestart the API container and monitor sending telemetry $ ./dev.sh restart api $ ./dev.sh logs -f api | grep email ok api restarted successfully # Streaming api logs matching 'email'... ok event="email_sent" provider="cloudflare" to="user@example.com"Success looks like
event="email_sent" provider="cloudflare". Failure logs the response body; usually an unverified-domain error or a permission-scope mistake.
Validating SPF / DKIM / DMARC
Section titled “Validating SPF / DKIM / DMARC”Once the dashboard shows the records active, verify them from your terminal:
$ dig +short TXT yourdomain.com | grep 'v=spf1'
$ dig +short TXT cf-xxxx._domainkey.yourdomain.com
$ dig +short TXT _dmarc.yourdomain.com
ok "v=spf1 include:cloudflare.net ~all"
ok "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQ..."
ok "v=DMARC1; p=quarantine; pct=100;"All three should return a value. If any are empty, the Cloudflare auto-provision didn’t complete; re-toggle Email Service in the dashboard.
Rotation
Section titled “Rotation”Every quarter, or after any staff change:
- Create a new API token with the same scope.
- Update
CLOUDFLARE_EMAIL_API_TOKENincompose/.env. ./dev.sh restart api.- Confirm a send works with the new token.
- Revoke the old token in the dashboard.
Switching to Resend / SendGrid
Section titled “Switching to Resend / SendGrid”The apps/api is provider-agnostic. Swapping is one env var:
$ echo 'EMAIL_PROVIDER=resend' >> compose/.env
$ echo 'RESEND_API_KEY=re_your_resend_api_key' >> compose/.envThe env validator refuses to boot in production if the matching key is missing. See Email and Cloudflare Email Service for the abstraction.
Source
Section titled “Source”- Provider implementation:
src/lib/email/providers/cloudflare.tsin the apps/api. - Cloudflare’s own docs: developers.cloudflare.com/email-service.
Related
Section titled “Related”- Cloudflare Email Service; why it’s the default and how it compares.
- Email; the pluggable provider abstraction behind every backend.
- Email in development; Mailpit when you don’t want to hit a real provider.
- Env validator; the boot-time check that catches a missing token before prod.