06-Jul-2026 — Committee mail delivered straight to your inbox, no Gmail needed #307

build 749commit b431f59


Delivers an alternative to the Gmail inbox: mail is delivered straight into the NGX inbox via Cloudflare Email Routing, with no per-group Gmail account, OAuth consent or Pub/Sub. Designed so several sites that share one Cloudflare zone - the ngx-ramblers.org.uk subdomains such as bolton and pang-valley - each get a fully independent inbox.

How to set it up: step-by-step in Setting up direct-to-inbox for committee replies, the companion to the Gmail inbox guide.

What you get

How mail reaches the right inbox (the router)

Setting up multiple sites on one shared zone

bolton.ngx-ramblers.org.uk, pang-valley.ngx-ramblers.org.uk etc. are subdomains of the single ngx-ramblers.org.uk Cloudflare zone. They share that zone's MX and its one catch-all. Cloudflare's subdomain Email Routing (MX already present per subdomain) makes @bolton..., @pang-valley... addresses deliverable. For each site:

  1. Cloudflare: the subdomain has Email Routing enabled and its committee addresses (secretary@sub..., walks@sub..., ...) exist as routing rules.
  2. In the site: Mail Settings -> Inbox -> provider = Direct to inbox.
  3. Redeploy the site once so it receives the shared router secret (NGX_INBOUND_SECRET - see below).
  4. Click "Route this site's committee mail into the inbox": this ensures the router worker exists and repoints the site's @ address rules at it.
  5. Mail to those addresses now lands in that site's Admin -> Inbox.

The catch-all (independent per subdomain)

Cloudflare provides exactly one catch-all per zone; there is no native per-subdomain catch-all (subdomain routing is done with per-address rules). To give every subdomain a real catch-all without letting any of them clobber the shared rule, the apex / platform-admin site points that single zone catch-all at the router worker (email-inbox-router). The router then routes anything@<subdomain> to that subdomain's own inbox by domain, so each subdomain effectively gets its own catch-all into its own inbox, with zero per-subdomain setup and no site able to change the shared rule. The per-site "Route to inbox" action above wires each site's known committee addresses; pointing the zone catch-all at the router additionally sweeps up everything else. Explicit per-address rules always take precedence, so the apex keeps its own role addresses (e.g. on Gmail) while the catch-all handles the rest.

Shared secret (how the platform gives every site the same key)

The router signs each POST with an HMAC secret and every receiving site validates it. It is a single value distributed through the existing platform-admin shared-secrets mechanism: secrets.NGX_INBOUND_SECRET in the central environments config is injected into every managed site as an env var at deploy (the same path AWS_* and AUTH_SECRET travel) and read via envConfig.value. A site only picks it up after one redeploy - hence step 3 above.

Subdomain catch-all guard

A guard refuses catch-all changes from a site whose baseDomain is a strict subdomain of its Cloudflare zone. This stops a subdomain site such as pang-valley.ngx-ramblers.org.uk changing the shared ngx-ramblers.org.uk catch-all and taking out staging and every other site on the zone. Apex / own-zone sites (Canterbury, staging) are unaffected and still own their catch-all.

What this commit adds

refactor+docs(aws): keep the file-upload route declarative

The /s3/file-upload route had an inline arrow function wrapping multer with branching and response building. Route files must stay declarative and delegate to named controller/handler functions.