Repository navigation
feat(platform): Administrator-managed email domains and installation-wide recipient policy - #330
Conversation
Keep the email record to current design and architecture: drop dated probes, fix-round narratives and old-CLI compatibility notes; add the planned self-hosted flow where an administrator runs the domain engine with the platform's runtime token and an installation-wide recipient policy relaxes the shared-sender allowlist. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ngs. An administrator can now decide who mail from the shared sender reaches (verified destinations, listed domains, or anyone), who registers email domains (owners or administrators), and a project's outbound caps — from the operator CLI or the admin UI. Settings are authoritative in the new platform_settings table; the proxy reads the policy from a routing KV mirror that fails closed to today's verified-only behavior. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…form credential. Administrators register and maintain projects' email domains from the operator CLI (`void platform email domain-add|domains|domain-status| domain-sync|domain-remove`) and the admin UI, running the same setup engine as owners. Such rows carry the new `platform` credential kind — no stored token; the engine, lease and teardown resolve them to the runtime token, and the zone must be in the platform's own account. Rows record `managed_by`; owners can read an administrator's row but are pointed at the operator command for sync and remove, and an administrator-managed platform refuses owner adds up front. The domain orchestration moves from routes/email.ts into email/domain-service.ts unchanged so both surfaces share one implementation of the serialized sections and compensations. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…latforms. `void email domain add` reads the installation's email settings first and, where administrators register domains, prints the operator command and stops instead of opening a Cloudflare consent. Domain output names the platform credential and administrator-managed rows. The installer's runtime-token link preselects the Email Routing, Zone Settings and Email Sending permissions when the runtime enables email. Docs: administration guide (recipient policy, caps, administrator-managed domains), self-hosting token permissions, email guide note, and the 0078 design record aligned with what shipped. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ipient policy. - Authorize add, sync and remove on the locked email_domains row, so an owner and an administrator can no longer interleave into each other's rows; an administrator never takes over an owner's row (409), from the admin form as well as the operator API. - Pin platform-credential rows to the platform's account on re-add and sync too, never only at first setup — the runtime token cannot be pointed at a customer zone through a pasted-token row. - Publish the recipient policy as delete mirror → write DB → put mirror in one transaction under an advisory lock shared with the repair: a KV failure leaves the proxy closed, never on an older, looser mirror, and a stale repair cannot republish an old policy over a new one. - Read `any` from the mirror only in the api's own shape; the admin settings page repairs a drifted mirror like every other read. - Surface a kept row after a failed removal as a failed operator mutation instead of nesting the failure under a success. - Drop a generated playground file swept into the previous commit. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…design record. Findings from the design-vs-implementation review: - Administrators may sync and remove an owner's row again (the ownership table's maintenance powers); only a re-add takeover stays refused. - An owner keeps renewing, syncing and removing a row registered before the switch to administrator-managed domains; only new domains are refused. - The recipient policy publishes in two locked steps — clear mirror + commit DB, then republish from the committed value — so the mirror can never run ahead of the database, and it never widens delivery to another tenant's address on the platform's own mail domain. - The proxy accepts only the api's exact mirror shape (`any` with an empty list, all-string lists within the cap). - The admin email pages sit behind the email capability like the API routes. - Sync on a platform row without a runtime token answers 503, not 500. - The CLI treats the settings route's own "project not found" as an error instead of falling back to self-serve and opening a browser for nothing. - Tests pin platform-row teardown, runtime-token loss on every surface, the reply-sender pin under `any`, and the platform-domain rule; the record and guides state the 60-second cache window and the remove path's lock scope. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f9d69d0b9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ep pre-switch owner rows renewable. The inbound gate excluded only EMAIL_SENDER_DOMAIN, so an unset binding or a second routed domain let `any` reach another tenant's inbox; the gate now carries the domain Cloudflare accepted the message on as well. The admin-mode refusal of a new owner domain moves into the insert's locked section, the owner CLI continues the renewal flow for a row it registered before the switch, and `status` on an administrator's row names the operator command for each step the owner cannot take. Record and guides aligned, including the rollback note. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…p the token triple complete. Workers KV refuses a second write to a key within a second, so the clear-then-put publish failed in production after committing; a publish is now a single write ordered by direction so the mirror is never looser than the database. The token-column CHECK requires all three values for every stored credential again. The owner CLI pins a renewal to the matched pre-switch row instead of retargeting on the apex MX, and `status` on a failed administrator row names `domain-add`. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e32050380f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… and probe platform rows by zone walk. An operator add or sync that ends with the row failed, revoked or expired now carries a top-level ok:false, so the audit records the failure and the CLI exits nonzero. The add preview names the stored mode on a re-run instead of warning about MX that setup itself installed, the policy preview and admin label disclose the platform-domain exception, and the platform-row status probe walks parent zones like setup does. Record and reference aligned, including the mixed-list publish exception. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6cb219db14
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…rite as failed mutations. The admin add form now inspects the returned row and answers 502 with the failing step when setup ends failed, revoked or expired, so the audit records the failure instead of a success. The operator limit verb writes each cap independently and reports which one landed when the other is refused. Policy descriptions across the operator preview, admin label and docs now say every address on the platform's own mail domain stays verified-destination-only, which is what the gate enforces. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Renumbers the email migrations to 0045_platform_settings and 0046_email_domains_platform_credential behind main's authentication migrations 0041-0044; SQL unchanged, snapshots regenerated from the merged schema. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 39e4371bc2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…l closed on a rejected policy read. A list that gains any domain now commits before the mirror is written, so the mirror never authorizes a domain the database has not committed. The proxy treats a rejected policy read as verified instead of failing the send or closing the inbound gate, and the CLI reads the email settings through the Access-aware request wrapper like its neighbours. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6a0f1a04fd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…main command. In admin mode the printed `void platform email domain-add` named the raw input, so an apex that already receives mail produced a command setup would refuse; the handoff now makes the owner flow's MX decision first and names the target setup accepts. Renewals still keep their stored target. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Summary
Aligns the platform email feature with the updated design record (
platform/meta/design-docs/0078-email-cf-native.md). Target scenario: one Cloudflare administrator deploys Void internally; staff (and their coding agents) deploy apps without a Cloudflare account.Design record
0078rewritten to describe the current design only (history / compat notes dropped); admin-managed domains and the recipient policy are folded in.Recipient policy (installation-wide)
verified(default) |domains [list]|any. Authoritative in newplatform_settingstable, mirrored to ROUTING KVemail-recipient-policy(60s cache). Absent or malformed mirror →verified.Administrator-managed domains
credential_kind: 'platform'→ engine / lease / teardown resolve toCF_API_TOKEN; zone must live inCF_ACCOUNT_ID(checked on setup, re-add, sync, on the locked row).--token-stdinfallback for zones elsewhere.managed_by: 'owner' | 'admin'. Owners get list/status on admin rows; sync/remove/re-add → 403. Admins may sync/remove owner rows; takeover by re-add → 409.email.domainssetting:self-serve(default) |admin. In admin mode the owner CLI prints the exact admin command; the server refuses only new owner domains.routes/email.tsintoemail/domain-service.tsso owner routes, operator routes and the admin UI share it.Limits
void platform email limitverb exposes the admin-set KV keys.Migrations
0041_platform_settings.sql,0042_email_domains_platform_credential.sql(nullabletoken_*with CHECK,managed_by).Rollout note
Pre-upgrade Workers cannot read
platformrows — register admin domains after the upgrade completes (documented in the administration guide).Test plan
vp run checkclean,vp lint0 errorsvp test --project platform-*— 168 files / 4313 testsGenerated with Devin