Blog

/

Cloudflare permissions for Flowmails, explained

Flowmails asks Cloudflare for a small, fixed set of OAuth scopes when you connect a domain. This is a per-scope tour of what each one is doing, why it has to be there, and what would break in practice if it were missing.

There is no API token to paste any more. The old flow asked you to hand-build a token with ten permission groups and paste a secret into a web app. Flowmails now uses an OAuth grant instead: you sign in, you approve a scope list on Cloudflare’s own consent screen, and the grant is short-lived. The list below is what it asks for.

The short version

Flowmails runs one shared mail worker in its own account. Your mail, your attachments and your route tables live there, scoped to your domain_id. What we need from your Cloudflare account is much narrower than what the old per-account worker needed: the ability to point your zone’s inbound mail at us, the ability to set up outbound for a domain, and the ability to deploy one small sender worker.

That is why the list is short. The old token asked for D1, KV, R2 and Workers Scripts write access, because the platform used to create a whole receive-worker — with its own database, its own key-value namespace and its own bucket — inside your account. It no longer does. The only thing deployed into a customer’s account is a ~120-line thin sender worker, and that needs exactly one account-level scope.

How the scopes are grouped

Cloudflare’s scope ids come from its own live catalogue, so they do not map one-to-one onto API-token permission names. The mapping is not 1:1 in either direction: umbrella permission families decompose (a token’s Workers Scripts group has no single OAuth counterpart), resources are renamed (R2 isworkers-r2.*, KV isworkers-kv-storage.*), and the action is not always read/write —metadata_read, content_read, admin and evaluate all exist alongside read / write. The list below is grouped by what each group is for.

DNS & Zones

Zone-scoped scopes — they act on a single Cloudflare zone, and they apply to every zone the grant can see.

zone.read

Zone: Read

The domain picker calls zones.list to enumerate every zone on the account. That call is the source of truth for the dropdown — the platform never enumerates zones from its own database, it always asks Cloudflare, so a stale row indomain can never make a zone appear that Cloudflare does not have. Read-only is correct: nothing in the connect flow needs to mutate the zone resource itself.

dns.read · dns.write

DNS: Read, Write

Enabling Email Routing replaces the zone’s MX records, and Cloudflare refuses to write them while foreign MX records are present. The platform reads the current records, removes the ones that block the takeover, and lets Cloudflare publish its own. Read is needed to inspect before it overwrites; write is needed to clear the obstruction.

The SPF, DKIM and DMARC records a sending domain needs are a different thing: Cloudflare publishes those itself on a cf-bounce.{DOMAIN} subdomain, and the platform asks Cloudflare to create or repair them through the Email Sending DNS endpoint rather than editing the zone’s records.

Email & Messaging

The two halves of Cloudflare’s mail products. They are separate products with separate onboarding, and missing either one fails a different half of the product.

email-routing-rule.read · email-routing-rule.write

Email Routing Rules: Read, Write

This is the inbound half, and without it you cannot attach a domain at all. The connect step installs a single catch-all matcher that forwards every address on the zone to rec_{domain_id}@flowmails.net, the per-domain address on Flowmails’ own mail domain. Cloudflare does the transport handoff; the shared worker does the rest.

Editing a route table does not call this scope. Which local parts a domain accepts lives in the shared worker’s key-value namespace, keyed route_rules:{domain_id}, and is checked on every inbound message. Adding or removing an address is a single write against that namespace — it never reaches Cloudflare’s API. The scope is used at three points only: zone setup, domain teardown, and the health check that inspects the rules already on a zone instead of blindly overwriting them. Read is included alongside Write for that check.

email-routing-address.read · email-routing-address.write

Email Routing Addresses: Read, Write

Cloudflare requires a destination address to beverified before it will route to it. Since the forwarding target is rec_{domain_id}@flowmails.net — a per-domain address on Flowmails’ domain — the platform has to create and verify that address on your account. These scopes cover that: /accounts/{id}/email/routing/addresses is account-scoped rather than zone-scoped, and the write is what creates the verified destination.

This scope did not exist under the old topology, because the old flow routed to a worker inside your own account and never needed a destination address on someone else’s domain. It is the cost of moving to one shared worker, and it is the reason a domain id has to be treated as a secret: anyone who learns your routing address can inject mail into your inbox.

email-sending.read · email-sending.write

Email Sending: Read, Write

Cloudflare treats inbound and outbound as two separate products. Email Routing (above) decides who receives mail; Email Sending decides who may send from your domain, and it has its own onboarding. A domain that was never onboarded onto Email Sending cannot send a single message — the send fails at Cloudflare’s edge with E_SENDER_NOT_VERIFIED.

The connect step calls POST /zones/{id}/email/sending/subdomains to onboard the domain, then reads GET /zones/{id}/email/sending/subdomains/{tag}/dns/status to learn whether the SPF, DKIM and DMARC records Cloudflare publishes are actually in place. The domain health check reads the same endpoint, and its one-click repair calls POST /zones/{id}/email/sending/subdomains/{tag}/dns, which re-applies the missing records. Read is included alongside Write because the check inspects before it overwrites, and because a zone whose DNS records were edited by hand has to be diagnosed before anything is rewritten.

One caveat worth knowing: Email Sending is Beta, and sending to recipients outside your account’s verified addresses needs a Workers Paid plan. The connect step never blocks on that — inbound keeps working either way, and the sending step is where the verdict is surfaced.

Developer platform

One account-level scope, and it exists for one narrow reason.

workers-scripts.admin

Workers Scripts: Admin

The only thing deployed into a customer’s account is the thin sender worker: a ~120-line Worker with a single send_email binding. It exists so the platform never needs a long-lived Cloudflare credential for sending, and so the customer never has to grant broad permissions to us. Deploying and removing it are the two calls this scope covers.

admin rather than edit is deliberate: Cloudflare’s “Editor” role can update and rename Workers but cannot create or delete them, and both happen here — first setup, and teardown.

Note what is not in this scope: no D1, no KV, no R2, no zone settings. The platform used to need all of those because it created a whole receive-worker with its own storage inside your account. It no longer does, and asking for those scopes would be asking for access to your data for no reason.

Account

Account-level scopes — the ones that act on resources that live at the account level rather than on any single zone.

memberships.read

Memberships: Read

GET /accounts has no dedicated scope; this is the cheapest documented match — read access to a user’s account memberships. The callback’s account resolution and the grant validation’s first probe both hit it, so a miss here fails the whole sign-in. Read only.

What is intentionally not on the list

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

  • D1, KV, R2 and zone settings — read or write. The platform creates no storage in your account. Your mail lives in Flowmails’ own worker, scoped to your domain, and asking for write access to a database you own would be asking for a capability with no use.
  • Anything that can rotate the grant itself. The grant Flowmails holds is the grant you approved. It cannot mint a replacement, and it expires on its own.
  • Account settings, members, or billing. The platform does not touch account-level billing, members, or audit log settings.

How long the grant lasts, and what happens when it ends

Cloudflare signs third-party OAuth clients with a short-lived access token — about an hour — and returns no refresh token. Its scope catalogue has no offline_access, so there is nothing to ask for.

The grant is stored server-side with an expiry stamped into the record itself, capped to the token’s own lifetime rather than to anything longer, and the key-value store deletes it when the window closes. Nothing depends on you remembering to revoke anything, and nothing claims a duration longer than Cloudflare actually grants us.

When the window closes, your mail keeps flowing — the shared worker and the thin sender worker both keep running. What stops working is the next action that needs to reach into your account: deploying the sender worker, or repairing a domain’s routing. At that point the dashboard offers you the same one-click sign-in again, and says plainly that it expired rather than claiming your credential was invalid.

How the grant is checked

Two different checks, and they do not fire at the same time. The one that runs every time the connect screen is opened is cheap and local: the platform resolves whichever credential it holds and reports which one answered. It makes no Cloudflare call, because all it needs to know is “is there a grant, and is it inside its window”. A grant that exists but has since been revoked surfaces at the first real action.

The check that exercises the scopes is the one that runs when a domain is attached. It makes live calls against Cloudflare and compares what comes back against the list above. A grant missing a scope is rejected before it overwrites the previous one, and the rejection names the scopes it is missing rather than saying “invalid token”.

Read the sibling post

The scope walkthrough above is the reference. If you want the decision rather than the list — what a short-lived grant guarantees you, what it does not, and how deletion works when the credential has expired — read what a 24-hour Cloudflare sign-in guarantees you.