Skip to content
BoringStack
GitHub

Cloudflare Email setup

5 min read

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.

  • 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.
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.

  1. 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.)

  2. 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).

  3. Capture the Account ID. Dashboard → any domain → right sidebar → “Account ID”. 32 hex characters. Drop it into compose/.env:

    Configure Account ID
    $ echo 'CLOUDFLARE_ACCOUNT_ID=your_32_hex_account_id' >> compose/.env
  4. Scope an API token. Dashboard → My Profile → API Tokens → Create Token → Custom Token with:

    • Permissions: Email Sending: Edit only, 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 Token
    $ echo 'CLOUDFLARE_EMAIL_API_TOKEN=your_scoped_api_token' >> compose/.env
  5. Set the sender + provider.

    Configure Outbound Sender & Provider
    $ echo 'EMAIL_PROVIDER=cloudflare' >> compose/.env
    $ echo 'EMAIL_FROM=noreply@yourdomain.com' >> compose/.env

    EMAIL_FROM must be on a domain you’ve enabled Email Service for. Sending from a domain that isn’t enabled returns a 403.

  6. Smoke-test.

    Smoke-Test Outbound Delivery
    $ ./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.

Once the dashboard shows the records active, verify them from your terminal:

Validate DNS Records
$ 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.

Every quarter, or after any staff change:

  1. Create a new API token with the same scope.
  2. Update CLOUDFLARE_EMAIL_API_TOKEN in compose/.env.
  3. ./dev.sh restart api.
  4. Confirm a send works with the new token.
  5. Revoke the old token in the dashboard.

The apps/api is provider-agnostic. Swapping is one env var:

Switch Provider to Resend
$ echo 'EMAIL_PROVIDER=resend' >> compose/.env
$ echo 'RESEND_API_KEY=re_your_resend_api_key' >> compose/.env

The env validator refuses to boot in production if the matching key is missing. See Email and Cloudflare Email Service for the abstraction.