Skip to content

What Mailda needs from your Cloudflare account

Every setting a Node depends on, in one place, with the two ways each can be put there.

The intended experience is that an operator never opens the Cloudflare dashboard. Mailda runs in the customer’s own account (ADR 7), and for a non-technical operator the dashboard is the hardest part of that premise. So mailda install does the account work at install with wrangler’s login, the one consent the operator already gave (wrangler-login-reach.md), and for changes made later from a browser the Node can carry its own grant (ADR 42, cloudflare-grant.md), which is optional. The dashboard path is kept, on purpose. A builder who wants to arrange the account by hand, or who already has, is told exactly what Mailda expects and how doctor checks it, rather than being asked to trust a button.

Two rules hold across the table:

  • The Node never assumes a setting is there. Each row names the doctor check that reads the truth from the account or from evidence, so a setting made either way is verified the same way.
  • Nothing here is a secret Mailda holds for you. The credential the install uses is your own wrangler login, carried on one request; the optional stored one is an API token you made, wrapped in your Node. The dashboard path involves no Mailda credential at all.
WhatWhy Mailda needs itFrom MaildaBy hand in the dashboarddoctor check
Workers Paid planFree forces 24-hour queue retention; a delivery event stuck a day is deleted silently (ADR 25)Cannot. A Worker cannot read its own account’s planBilling → Workers & Pages → Paidworkers_paid_plan reports unverified and names where to look. This is the one row nothing can verify.
The Worker and its bindings: D1 CATALOG, R2 EVIDENCE, Durable Objects, the SENDING_EVENTS queue, the BUTLER_RUNS Workflow, the every-minute cronThe Node’s own storage and machinerymailda deploy, or the Deploy button. Wrangler provisions every binding from wrangler.jsonc with no account-specific id (ADR 24)Not recommended. The names derive from the Worker’s, and a hand-made resource is one wrangler will not adopt (deploy-plan)catalog_reachable, evidence_bucket_reachable, migrations_applied, key_vault, butler_execution
Schema appliedAn empty catalog answers 500Automatic on claim, or POST /api/preparewrangler d1 migrations apply CATALOG --remotemigrations_applied
The Node’s own credential, optionalChanging the Cloudflare setup from the browser later: another receiving domain, a sending domain, a routing-rule takeover, buying a domain. The install and the upgrade need none of it, they carry wrangler’s login on one requestPUT /api/provider/token (/setup → connect this Node; mailda provider --token, read from stdin): an API token, verified against GET /user/tokens/verify, bound to the one account it sees, stored wrapped like the sending token; DELETE forgets it. Permissions: Account Settings Read, Zone Read, Zone Settings Edit, Email Routing Rules Edit, Queues Edit, the Email Sending group; Registrar Domains Read only to buy domains; Email Routing Addresses Read only to read which recipients are verified destinationsMy Profile → API Tokens → Create Token → Custom, those permissions, restricted to this account; paste it once. Superseded the OAuth client on 26 September 2026 (ADR 42)provider_token reports no_token, token_held or token_refused
Email Routing on the zone, MX and SPF records, and a rule routing your address to this WorkerInbound mail reaches the Node only through a routing rule to its email handlerAt install, with wrangler’s login (mailda install; mailda upgrade for a Node never set up): the same route with the operator’s credential on the request. Later, POST /api/provider/receiving through the grant (/setup → receiving; mailda provider --onboard-receiving). Proposes the records and the rule, confirmed by digest, enables routing on the zone if it is off (#209, #210). The records are Email Routing’s own: the subdomain-records endpoint names what is missing and, after enabling, lists what is there, and an apex’s records come with enabling routing on the zone; raw DNS is not read or written for receiving (measured 25 and 26 September 2026, wrangler-login-reach.md). A rule that already routes the address elsewhere is listed and taken over from the same screen, with a put-back (#258, GET /api/provider/routing-rules). On a zone’s own name the proposal offers the catch-all instead (25 September 2026): one rule, every address without a rule of its own managed on People, and the proposal lists the addresses that keep one and where each goes (28 September 2026); on a subdomain Cloudflare allows literal rules only, and adding an address on People writes its rule in the same actEmail → Email Routing → enable; DNS → add the MX and SPF records it shows; Routing rules → Send to a Worker → this Node’s Worker, or on an apex the catch-all → Send to a Worker. Taking a subdomain back out of routing is dashboard-only unless a grant with dns.write exists: wrangler’s login may create those records and may not delete them (measured)inbound_routing, from evidence: addresses that have received, and silence since
A sending domain onboarded for Email Sending, with DKIM, SPF and DMARC recordsArbitrary recipients require a verified sending domain; paying for Workers is not enoughAt install, with wrangler’s login (mailda install; mailda upgrade). Later, POST /api/provider/sending through the grant (/setup → sending; mailda provider --onboard-sending). Records proposed, confirmed by digest, verified by reading them backEmail → Email Sending → add domain; DNS → add the records it showstransport_adapters (which adapter can carry mail), and the send ladder itself: handed_over → accepted / bounced
The send_email binding, or REST credentialsThe transportThe binding ships in wrangler.jsonc; REST credentials are the fallback for a Node that cannot be redeployed: PUT /api/transportWorkers → Settings → Bindings, or an Email Sending API token pasted into PUT /api/transporttransport_adapters
Delivery outcomes: the email.sending event subscription publishing to SENDING_EVENTS, and a consumer on that queueWithout both, every send sits unobserved forever and nothing looks wrongAt install, with wrangler’s login (mailda install; mailda upgrade). Later, the subscription: POST /api/provider/subscription through the grant (/setup → delivery outcomes; mailda provider --subscribe <domain>), proposed and confirmed by digest, for a domain already onboarded for sending (receipt). GET /api/provider/delivery-events names which of the three is missing. The same confirm attaches this Worker as the queue’s consumer when nothing consumes it (measured 16 September 2026: POST /accounts/{id}/queues/{id}/consumers); queue:attach-consumer remains for a Node with no grant. One case no subscription fixes: in the one case measured, mail to a verified destination address of the account produced no delivery event at all (receipt, 28 September 2026). POST /api/provider/verified-destinations reads which of the Node’s recipients those are and records it (mailda setup and mailda upgrade with wrangler’s login; /setup → delivery outcomes with a token carrying the optional Email Routing Addresses Read), so the Outbox marks them verified destination and delivery_visibility does not call that silence blindQueues → the SENDING_EVENTS queue → Settings → consumer: this Worker. The subscription: Email → Email Sending → the domain → events → this queuesending_events_consumer, delivery_visibility, delivery_attribution, delivery_explanation_void
A domain, if you do not have oneOptionalGET /api/provider/domains to price, POST /api/provider/domains/purchase confirmed by digest (mailda provider --buy)Domain Registrationnone
A hostname for the NodeThe Node answers on <worker>.<account>.workers.dev by default; a name of your own reads better on every link the Node hands outThe install and the upgrade ask for one (a zone from the list, then the label) and write it into the Node’s derived config; a first deploy attaches it, the canary path keeps it, and an upgrade adding one runs wrangler triggers deploy (receipt), with workers_dev: true beside it so the workers.dev address stays. An upgrade asks only when Cloudflare lists no custom domain on the Worker, and checks the canary at one when it does. Remembered in .mailda/nodes.jsonWorkers → Settings → Domains & Routesnone: the hostname the Node is reached on is whatever answered; nothing in the Node depends on which
  • From Mailda means the Node does it through its own grant, shows what it is about to do, and acts only on a confirmation bound to a digest of that exact proposal (cloudflare-grant.md, Onboarding a domain for sending). It never acts on a stale plan.
  • By hand is the same end state. The Node does not know or care which path produced it; doctor reads the account, or the evidence, either way.
  • One row needs a person at the provider, the optional token, and only when the browser is to change the Cloudflare setup later: API tokens are made in the dashboard, one form, restricted to this account. The ordinary path needs no row of the dashboard at all (docs/receipts/wrangler-login-reach.md). The email.sending subscription was on this list until 16 September 2026, when the API turned out to create one; the Node creates it now (#222).
  • One row cannot be verified at all: the plan. doctor says so rather than reporting ok.
  • Attaching the consumer through the Node’s token is unmeasured. The API attaches it (measured with the operator’s token), the live Node’s consumer was already attached by the deploy, and no Node without one has been available to try. Queues: Edit is the candidate.
  • A token short of a permission registers anyway. GET /user/tokens/verify reports status and expiry, not permissions, so a token missing Email Routing Rules: Edit is refused by Cloudflare at the first rule it writes, in Cloudflare’s words, and /setup and mailda provider say so beforehand rather than checking. Measured 16 September 2026 with the OAuth grant this replaced: the routing rule needs its own write permission, and a credential that wrote the MX records and was refused the rule is how the receiving proposal learned to resume its own half-done work rather than refuse it. Raw DNS is not asked for at all: receiving reads and writes Email Routing’s own records through its own endpoints.
  • A custom hostname from Mailda, so the last dashboard-only row goes.