09-Jul-2026 — Sites that share a domain each get their own committee inbox ref #307
build 758 — commit fddc67a
What this gives you
A group whose website is a subdomain of a shared domain (for example kent.ngx-ramblers.org.uk, which sits under ngx-ramblers.org.uk) can now receive all of its committee mail straight into the NGX inbox - with no Gmail account, no Google Cloud project and no manual Cloudflare steps. The main site on that shared domain carries on using Gmail exactly as before.
Behind it is a single "shared inbox router" set once on the main (apex) site. For every message it works out which site the address belongs to and either delivers it into that site's inbox (for a site on Direct-to-inbox) or forwards it on. Nothing is ever lost: if a site can't be reached, mail goes to a safety-net address instead of bouncing.
What you'll see in the admin
- On the main site (Committee Settings Email routing) the catch-all gains a "Shared inbox router" option. You set it once, choose where this site's own unmatched mail goes (deliver to the inbox, forward to an address, or drop), and give a safety-net address that is only used if a site on the zone can't be reached.
- On a subdomain site the catch-all card explains that delivery is provided by the parent domain's router, and offers the same three choices for that site's own unmatched mail
- The misleading "no committee addresses found" button is gone.
- Every site, Gmail or Direct-to-inbox, gets the same "notify the assigned member when new mail arrives for their role" controls (no notification / email the member / send to a different address). Mail configuration now lives in one place under Mail Settings; the confusing duplicate under System Settings has been removed.
- Editing a committee role no longer hides the catch-all card, and the apex catch-all saves everything with a single Save / Cancel.
- A group that just forwards its committee mail (no NGX inbox) now has an honest "No inbox" option in Mail Settings, and it is the default when nothing has been set up, so a forwarding-only group is no longer shown Gmail setup or offered the "route into the inbox" action by default, and keeps its per-role Inbound Forwarding controls.
- The roles table's Email Forward pill now reflects each role's own forwarding choice when its rule is delivered by a worker: a role forwarding to one address (member's personal email or a custom address) shows Active with that destination, and "Multiple" with a recipient count is reserved for roles actually set to multiple recipients (previously any worker-delivered rule showed "Multiple > 0 recipients").
Safety
- Mail is never dropped: an unreachable site, an error, or a site not on Direct-to-inbox all forward to the safety-net address rather than bouncing. This removes the loss behaviour behind the 2026-07-06 incident.
- Committee addresses that already have their own routing rules are left untouched.
Technical notes
- A shared email-inbox-router worker fans out by recipient domain to each site's /inbound-inbox; each site decides store-vs-forward from its own inbox.catchAll policy and provider, so every site stays authoritative for its own config.
- New SHARED_ROUTER catch-all action (apex-only, guarded to the zone owner); GET /config resolves zone ownership (ownsZone/zoneName) so the client can tell an apex from a subdomain, and the setup view degrades safely when ownership can't be resolved.
- Direct-to-inbox filing keys strictly on a role's own address, so a message is stored only under the role it was sent to (previously it could appear under every role a member held); a leftover Gmail mailbox connection no longer 500s the inbound webhook.
- Notification delivery was already provider-agnostic (the inbox digest cron); this exposes the config for Direct-to-inbox too and makes the panel self-persist. Mail Settings now persists provider and member changes.
- The shared per-site catch-all state/save is used by both apex and subdomains; the "fallback" is re-scoped as a safety net distinct from the destination choice.
- New InboxReaderProvider.NONE (forwarding-only): the default when no provider is set and no Gmail OAuth is configured, so a forwarding group reads as "no inbox" rather than being pushed toward Gmail; internalInbox stays false for it, so per-role Inbound Forwarding remains editable.
- Router-freshness check is scoped to the catch-all context, so a committee role whose rule was wrongly repointed onto the shared router is correctly flagged out-of-date (redeployable) rather than reported up-to-date.
- Smaller fixes: unread-badge re-sync on delete/read, stale-thread 404 handled gracefully (clears the selection instead of erroring), and SectionToggleTab moved into a model file.