Skip to content
BoringStack
Star

Cloudflare Email setup

3 min read

Set up Cloudflare Email Service to send transactional mail from your domain. This is a one-time setup; token rotation afterwards is step 4 only. The setup auto-provisions SPF, DKIM, and DMARC because your zone is on Cloudflare.

For the reasoning behind choosing Cloudflare as the default email provider, see Cloudflare Email Service.

  • A Cloudflare account that owns the domain you’ll send from.
  • Admin access to that account.
  • A real email address for audit logs on the API token.

Cloudflare dashboard → Workers & Pages → Plans → Upgrade to Workers Paid.

Email Service is bundled; no separate charge. Check current pricing for the current cost.

Dashboard → Email → Email Routing (or Email Sending) → enable for your domain.

Cloudflare auto-provisions SPF, DKIM, and DMARC records. Wait for the dashboard to show all three as Active (usually under a minute). This is the step that eliminates manual DNS hand-edits, where most email setups fail.

Dashboard → your domain → right sidebar → Account ID (32 hex characters).

Add it to compose/.env:

Terminal window
echo 'CLOUDFLARE_ACCOUNT_ID=your_32_hex_account_id' >> compose/.env

Dashboard → My Profile → API Tokens → Create Token → Custom Token.

Permissions: set Email Sending to Edit only. Account resources: include this specific account. TTL: leave indefinite; you’ll rotate quarterly.

Copy the token (you won’t see it again) and add it to compose/.env:

Terminal window
echo 'CLOUDFLARE_EMAIL_API_TOKEN=your_scoped_token' >> compose/.env
Terminal window
echo 'EMAIL_PROVIDER=cloudflare' >> compose/.env
echo 'EMAIL_FROM=noreply@yourdomain.com' >> compose/.env

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

Restart the API and watch for a send:

Terminal window
./dev.sh restart api
./dev.sh logs -f api | grep email

Expected output:

event="email_sent" provider="cloudflare" to="user@example.com"

If it fails, check the logs for the response body. Common mistakes: unverified domain, or missing Email Sending permission on the API token.

After the dashboard shows records active, verify them from the terminal:

Terminal window
dig +short TXT yourdomain.com | grep 'v=spf1'
dig +short TXT cf-xxxx._domainkey.yourdomain.com
dig +short TXT _dmarc.yourdomain.com

All three should return a value. If any are empty, re-toggle Email Service in the dashboard.

Every quarter, or after any staff change:

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

The API is provider-agnostic. Change one env var:

Terminal window
echo 'EMAIL_PROVIDER=resend' >> compose/.env
echo 'RESEND_API_KEY=re_your_resend_api_key' >> compose/.env

The env validator will refuse to boot in production if the matching key is missing. See Email for the provider abstraction.