Blog

/

Cloudflare API token permissions, explained

The onboarding screen asks for an unusually long list of permissions on a Cloudflare API token. This is a per-permission tour of what each one is doing, why it has to be there, and what would break in practice if it were missing.

The short version

Flowmails does not own a mail server. It does not run its own database. It does not even host the worker that processes your inbound mail. Every one of those pieces is provisioned on your Cloudflare account, using your token, on your behalf. The permission list reflects that: it is the union of everything the platform has to be allowed to touch while bringing a fresh domain online and keeping it that way.

Cloudflare's API token model is now organised into two scopes — All domains and Entire account — each broken into a handful of sub-scopes that group related permissions. There is no “Operator” preset that maps cleanly onto what Flowmails needs, so the walkthrough below is the smallest set we have been able to land on after a few rounds of trimming, grouped the same way the new token page groups them.

How Cloudflare groups these

The token creation page now presents nine permissions split across two scopes:

  • All domains — permissions that act on a single zone, scoped automatically to every zone the token can see. Two sub-scopes: DNS & Zones (3 permissions) and Email & Messaging (1 permission).
  • Entire account — permissions that act on resources that live at the account level, not on any single zone. Two sub-scopes: Developer platform (4 permissions) and Account & Billing (1 permission).

The walkthrough below follows the same grouping: two scope sections, each split into sub-scopes, each sub-scope into per-permission articles in the order the new token page displays them.

All domains

Zone-scoped permissions — these grant access to resources that live on a single Cloudflare zone, and they apply to every domain the token can see.

DNS & Zones (3)

DNS: Read, Edit

Honest disclosure: this is one of the entries on the list that is not currently exercised by an outbound API call in the onboarding flow. Theflowmails-r2.{DOMAIN}hostname is still created via the R2 custom-domain API, not the DNS edit endpoint, so DNS write is not part of the bring-a-domain-online path.

It is kept on the required list as defense-in-depth: the platform already auto-rewrites third-party MX records out of the way when Email Routing needs the zone, and a few upcoming flows (SPF / DKIM monitoring, custom verification records for the outbound worker) will land here in the next two releases. We would rather the verify step fail at token-save time than silently no-op the first time one of those calls needs to run. The bar for staying on the required-permissions list is “we will use this in the next two releases.”

Zone: Read

The Select Domains step callszones.list to enumerate every zone on the account. That call is the source of truth for the dropdown — we never enumerate zones from D1, we always ask Cloudflare, so the row indomain is purely a “this user provisioned this zone” marker, not a zone cache. Read-only is correct here: nothing in the onboarding flow needs to mutate the zone resource itself.

Zone Settings: Read, Edit

Honest disclosure: this is the other entry on the list that is not currently exercised by an outbound API call in the onboarding flow. It is kept on the required list as defense-in-depth — the R2 custom-subdomain binding and a handful of zone-level settings (DNSSEC, development mode, crawl hints) are areas the platform will touch as the surface grows, and we would rather the verify step fail at token-save time than silently no-op when those calls land.

If we end the quarter without a concrete call against it, it should come off the list. The bar for staying on the required-permissions list is “we will use this in the next two releases.”

Email & Messaging (1)

Email Routing Rules: Read, Edit

The final onboarding step callsPOST /zones/{id}/email/routingto wire inbound mail for the zone into the deployed worker. The payload is a single catch-all matcher that forwards every address on the zone to a worker action — Cloudflare Email Routing does the transport handoff, the worker does the rest. Read is included so a re-run of the step can inspect the existing rule instead of blindly overwriting.

Entire account

Account-scoped permissions — these grant access to resources that live at the Cloudflare account level, not on any single zone. One set covers every domain on the account.

Developer platform (4)

D1: Read, Edit

Onboarding provisions a D1 database calledflowmails-dbon the chosen Cloudflare account. The list-then-create sequence (d1.database.list followed byd1.database.create when missing) needs Read and Edit. The resulting database ID is then wired into the deployed worker as ad1 binding, which is where the receive worker persists inbound message metadata.

Edit is required even though onboarding only creates a database once, because Cloudflare's API treats the list/create pairing as a single “manage databases” scope. There is no read-only-D1-create permission to pick out of the group.

Workers KV Storage: Read, Edit

The receive worker is bound to a KV namespace calledflowmails-kv. The provisioning step lists existing namespaces and creates one if it is missing — same shape as D1, same reason for needing Edit alongside Read. The worker uses the namespace for short-lived routing state and idempotency keys, which is why the binding is created up front rather than lazily.

Workers R2 Storage: Read, Edit

R2 is where the platform stores raw message payloads. The provisioning step creates a bucket calledflowmails-bucket in the chosen account and binds it to the deployed worker. After that, the per-zonesetR2CustomSubdomaincall bindsflowmails-r2.{DOMAIN}to the bucket so each zone has a stable, branded hostname for the storage layer.

The R2 pre-check in the onboarding UI is the famouscode: 10042 error from Cloudflare: until R2 is enabled once in the dashboard, even a token with the right permission groups will be rejected at the gateway. That is not a Flowmails bug — it is a Cloudflare account-state requirement, and the amber callout on the token step exists specifically to surface it before the user hits a confusing failure.

Workers Scripts: Read, Edit

This is the permission that does the heaviest lifting. The onboarding deploy step uploads the receive worker script (workers.scripts.update), enables the*.workers.devsubdomain (workers.scripts.subdomain.create), and resolves the account-level worker subdomain (workers.subdomains.get). All three calls need Edit at the account level. The receive worker itself is bundled byapps/email-worker/build.js(esbuild), copied intoapps/web/app/server/email-worker/worker.js, and inlined into the web server bundle as a?rawimport — there is no placeholder, and no later overwrite step.

Note that the worker is deployed per account, not per zone. If you onboard multiple zones that share a Cloudflare account, they all share one worker, and the onboarding flow models that as a single deploy step with a list of zones going in.

Account & Billing (1)

Account API Tokens: Read

The verify flow calls/accounts/{id}/tokens/verifyand/accounts/{id}/tokens/{tokenId}to read back the token's own policy list. Without this, we cannot tell which permission groups a freshly-saved token has been granted, and we cannot produce a useful “missing permissions” error if the user got the list wrong. The verify step would have to fall back to a stringly-typed sniff of the Cloudflare error message — we did that for a while, and it is the most fragile part of the onboarding code, so the explicit read permission is well worth the cost.

Read only. The token cannot edit its own policies, rotate itself, or create new tokens through this scope.

What is intentionally not on the list

A few things that are sometimes expected, and that the token does not have, on purpose:

  • Account: Workers Tail Workers. Tail workers are a debugging surface that the dashboard does not currently consume.
  • Account: Account Settings. The platform does not touch account-level billing, members, or audit log settings.
  • Anything that can rotate the token itself.The token that Flowmails uses is the token the user pasted. It cannot mint a replacement.

Why we verify on every visit

The first thing the token step does on mount is fireverifyStoredTokenagainst whatever is already in the database, not just whatever the user has just pasted. This is deliberate: a Cloudflare API token can be revoked, downscoped, or expire between sessions, and the only way to know it is still good is to ask Cloudflare. If the round-trip says the token is healthy and still has every required permission group, the step auto-advances after a short countdown so a returning user does not have to redo the form. If the round-trip says the token is no longer valid, the user lands back in the form with a banner that names the missing pieces.

The verify path makes four sequential calls against Cloudflare — list accounts, verify the token, fetch the token policy, and enumerate the account's permission groups. They are not parallelised because each step feeds the next, but in practice the round-trip is fast enough that the spinner is the part of the UX that takes longest, not the network.

Rotating the token

If you need to rotate — for any reason, from a leaked clipboard to a routine team-policy rotation — the path is: create a new token in the Cloudflare dashboard with the same permission set, then paste it into the token step. The save action runs the same verify path that the on-mount verify uses, so a token that is missing a required group is rejected before it overwrites the old one. The old token is not revoked by the platform; you do that from the Cloudflare dashboard after the new one is confirmed working.