30-Jul-2026 — Deliverability by Design: Sender Reputation Isolation, Authentication and Keeping Mail out of Spam
Getting an email delivered to the inbox — not the spam folder, not a bounce, not a silent block — is not one setting you switch on. It is a chain: the domain has to be authenticated, the sending reputation has to be healthy, the receiving provider has to trust the message, and when something does go wrong the system has to notice and adapt. This article walks that chain end to end and shows what NGX Ramblers has built at each link.
The transport architecture underneath all of this — outbound through Brevo, inbound through Cloudflare — is already covered in Email at Scale: Outbound via Brevo, Inbound via Cloudflare, and the shared-inbox setup guides (Gmail and direct-to-inbox) cover the plumbing. This piece assumes those and focuses on the thing they touch only lightly: deliverability — the engineering that keeps legitimate mail flowing and keeps a problem at one group from becoming everyone's problem.
Why sender-reputation isolation matters
Every mailbox provider — Gmail, Outlook, Apple — decides whether to trust a message partly on the reputation of the sending domain. Reputation is earned and lost per domain: a run of spam complaints, or a receiving provider blocklisting a sender, attaches to the domain that sent the mail.
NGX gives every group its own instance. That is not only a hosting decision — it is a deliverability decision. Each group runs as its own Fly application, backed by its own MongoDB database, holding its own Brevo configuration (API key, SMTP credentials, sender identities) and — critically — its own DKIM-signed sending domain. There is no shared Brevo account and no shared sending domain across groups.
flowchart TB
subgraph groupA["Group A instance"]
A_APP@{ icon: "ngx:express", label: "Own Fly app + Mongo + Brevo key", pos: "b", h: 48 }
A_DOM["group-a.org.uk<br/>own SPF · DKIM · DMARC"]
A_APP --> A_DOM
end
subgraph groupB["Group B instance"]
B_APP@{ icon: "ngx:express", label: "Own Fly app + Mongo + Brevo key", pos: "b", h: 48 }
B_DOM["group-b.org.uk<br/>own SPF · DKIM · DMARC"]
B_APP --> B_DOM
end
A_DOM -->|"sender reputation A"| PROV@{ icon: "ngx:user", label: "Receiving mail providers", pos: "b", h: 48 }
B_DOM -->|"sender reputation B"| PROV
style groupA fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
style groupB fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
The consequence is containment. If one group's mail attracts spam complaints, or a provider decides to block its domain, the blast radius stops at that group. No other group's deliverability moves, because no other group shares the reputation. The separation is built in at the sending layer, not only the hosting layer.
The instance registry that makes this true lives in config.environments (the ENVIRONMENTS config key): each EnvironmentConfig carries its own Fly app, its own Mongo cluster and credentials, and its own secrets. When a new environment is created, one of the provisioning steps is "Authenticate Brevo sending domain via Cloudflare DNS?", which writes that group's own DKIM, SPF and DMARC records. Its own domain, its own reputation, from day one.
This per-group-instance model is exactly what makes the reputation containment real: a group is a self-contained unit — its own app, its own database, its own sending domain — rather than one tenant sharing infrastructure with others.
Authentication as a live health model
Three DNS-level mechanisms let a receiving provider trust that a message really came from the domain it claims:
| Mechanism | What it proves | How NGX manages it |
|---|---|---|
| SPF | The sending server is authorised for the domain | Checked against include:_spf.mx.cloudflare.net and include:spf.brevo.com; a single merged record is enforced (multiple SPF records are an RFC violation and flagged) |
| DKIM | The message was not tampered with in transit | Provisioned through Brevo's domain authentication; the DKIM and verification records are written into Cloudflare and Brevo's own authenticate/verify status is read back |
| DMARC | Combines SPF and DKIM under a stated policy | Checked at _dmarc.<domain> (inheriting from the organisational domain if absent), parsing the policy and whether reporting is configured |
| MX | Inbound mail is routed correctly (for reply-handling) | Checked against Cloudflare's route1/route2/route3.mx.cloudflare.net at the expected priorities |
What makes this more than a one-time setup is that NGX treats authentication as a live health model, not a checklist someone ticked once. The Mail Settings → Domains tab queries the real DNS state through the Cloudflare API on every visit and renders it as status badges — SPF OK, DMARC none, All MX records present — with the exact records it would create shown inline. Where something is missing, the same screen offers the fix:
- Authenticate Domain registers and verifies the domain with Brevo and writes the DKIM records.
- Fix Auth Records creates or merges the SPF include and the DMARC record.
- Add Missing MX Records completes the inbound routing.
- Switch sending domain re-authenticates a new domain and rewrites every sender identity from the old domain to the new one.
One honest detail worth stating plainly: the default DMARC policy NGX writes is p=none with reporting pointed at Brevo (rua=mailto:rua@dmarc.brevo.com). That is a deliberate monitoring posture, not an oversight — p=none lets a domain publish DMARC and collect reports without risking legitimate mail being quarantined while alignment is still being confirmed. Moving a warmed, confirmed domain on to p=quarantine is a later hardening step, taken per domain once the reports are clean.
The SPF and DMARC record management, the domain-health checking and the Brevo domain-authentication flow were built as part of the deliverability work in #186.
When authenticated mail still gets junked
Here is the part that surprises people, and it is the reason the next two sections exist: clean authentication is necessary but not sufficient. A domain can pass SPF, DKIM and DMARC and still have its mail dropped into spam, because providers weigh behaviour as well as identity.
The signals that push a perfectly authenticated message towards the spam folder are familiar once you know to look for them:
- A cold sending domain. A brand-new subdomain with no sending history has no reputation yet. Early messages from it are treated with suspicion until enough good mail has flowed to establish trust.
- Campaign-style markup on a one-to-one message. A transactional note carrying an open-tracking pixel and every link rewritten through a click-tracking redirect looks, to a spam filter, like a marketing blast rather than a personal reply.
- A message that claims your own domain but arrives via a third party. Mail sent from an address on an organisation's own domain, to another address on that same domain, but routed through an external sending service, trips the "is this a self-spoof?" heuristic that providers apply aggressively.
None of these is an authentication fault. They are behavioural, and they matter most for new domains and test sends. The practical upshot: a deliverability strategy cannot stop at "the DNS is green". It has to account for a receiving provider junking legitimate mail anyway — and pulling that mail back out of spam is exactly what the shared inbox now does.
The shared inbox: rescuing legitimate mail from spam
When a group runs a shared inbox, NGX reads the connected mailbox and files each conversation into an inbox or a junk folder. Junk threads are deliberately quiet: they never raise a notification, never increment an unread badge, and never appear in the normal inbox view. That is correct for real spam — but it means a legitimate message wrongly filed as spam by the provider would simply vanish from view.
So the spam scan does more than file. For every message the provider has put in its spam folder, it reads the Authentication-Results header the receiving server stamped on, and applies a two-part test:
flowchart TB
START@{ icon: "ngx:brevo", label: "Message in provider spam", pos: "b", h: 48 }
AUTH{"DMARC pass?"}
OWN{"From one of<br/>our own domains?"}
EXIST{"Already filed<br/>as junk?"}
RESCUE["Remove SPAM label ·<br/>place in inbox ·<br/>mark unread"]
KEEP["Leave in junk ·<br/>no notification"]
START --> AUTH
AUTH -->|"no"| KEEP
AUTH -->|"yes"| OWN
OWN -->|"no"| KEEP
OWN -->|"yes"| EXIST
EXIST -->|"yes — self-heal"| RESCUE
EXIST -->|"no — fresh"| RESCUE
A message is rescued only when it passed DMARC and it is from one of our own group domains. Both conditions matter, and the pairing is what makes it safe:
- The own-domain check means the system only ever rescues our mail, never someone else's spam.
- The DMARC-pass check is the anti-spoof guard. A forged message pretending to be from our domain fails DMARC — which is precisely why the provider put it in spam — so it is never pulled back out. The rescue can only lift mail whose identity is genuinely proven.
When both hold, NGX removes the provider's spam label and files the message in the inbox as unread, so it shows up as new. The scan is also self-healing: a message that was already filed as junk before it qualified — or before this capability existed — is re-homed to the inbox on the next scan, with no manual step. Where a human wants to override the machine, the manual Not junk action does the same thing on demand.
The digest that emails committee members about new inbox mail was corrected to match: it now excludes junk-folder threads entirely and marks them notified, so nobody is ever pinged about a message that is sitting in spam and invisible in the inbox. The count and the visible inbox always agree.
The shared inbox itself is the subject of the setup guides linked below — #278 for the Gmail model and #307 for direct-to-inbox — while the spam-rescue, self-heal and digest correction described in this section were added under #186.
Feedback loops: bounces and complaints
Sending reputation is not only about getting the first message through — it is about listening to what comes back. Continuing to mail an address that hard-bounces, or someone who marked you as spam, actively damages reputation. NGX closes that loop through Brevo's events webhook.
flowchart LR
PROV@{ icon: "ngx:user", label: "Receiving provider", pos: "b", h: 48 }
BREVO@{ icon: "ngx:brevo", label: "Brevo events webhook", pos: "b", h: 48 }
subgraph ngx["NGX Ramblers"]
BLOCK["emailBlock record +<br/>subscriptions → false"]
DB@{ icon: "ngx:mongodb", label: "Durable local suppression", pos: "b", h: 48 }
BLOCK --> DB
end
HQ@{ icon: "ngx:ramblers-hq", label: "Ramblers API /bounced_email", pos: "b", h: 48 }
PROV -->|"hard bounce · spam · complaint · block"| BREVO
BREVO --> BLOCK
BREVO -->|"hard → report + suppress<br/>soft → report only"| HQ
style ngx fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
When a hard bounce, spam complaint, block or unsubscribe arrives, NGX writes an emailBlock record against the member, recording the reason and source, and flips the matching list subscriptions to unsubscribed. This is a durable local suppression list — held in the member's own record, independent of Brevo — so the group keeps a permanent, queryable account of who must not be mailed and why. A soft bounce is treated more gently: it is reported but does not suppress, because a soft bounce is often transient. The Brevo events webhook, the local suppression and the blocklist sync were built under #18.
Two further deliverability courtesies sit on the outbound side. Every transactional email carries a List-Unsubscribe header and the one-click List-Unsubscribe-Post variant (RFC 8058), so a recipient's mail client can offer a proper unsubscribe button — which keeps people from reaching for the spam button instead, the single worst thing for reputation. And every HTML email is sent with a plain-text alternative derived automatically, because HTML-only messages are themselves a mild spam signal. Both were added under #186.
Where a member's consent is fully withdrawn — their last active subscription drops to zero — that transition is written back to the shared Ramblers Team Emails API, and hard bounces and complaints are reported to the same API's /bounced_email endpoint (idempotent, with bounded retries). The consent writeback came with the Brevo SDK v5 and subscription-audit work in #168; the bounce and complaint reporting arrived when NGX adopted the Ramblers Team Emails API in #331. The full member and consent flow between NGX, Brevo and the Ramblers Team Emails API is its own subject, covered in Brevo Member Sync: How the Information Flow Changes.
Operational resilience: keeping the plumbing alive
The last link in the chain is the least glamorous and the easiest to forget: the sending machinery has to stay working, and failures have to be noticed before a member does.
- SMTP key keepalive. Brevo deactivates an SMTP key after three months of no send activity. One NGX path — the "Brevo re-send" relay for certain committee addresses — can legitimately go that long between messages, so the key would expire from pure disuse and inbound mail to those addresses would start bouncing. A scheduled monthly probe sends a keepalive through the key, resetting the clock and confirming the relay still works (#341).
- Inbox token health and watch renewal. For Gmail-backed inboxes, a daily job renews the Gmail watch (which Google expires after seven days) and checks each OAuth token's health, flagging a revoked token on the mailbox status and alerting rather than failing silently.
- A scheduled-task framework with memory. Every one of these jobs runs under a registry that keeps run history, detects overdue schedules, restarts stopped crons through a watchdog, and alerts platform administrators when a task fails, is missed, or is interrupted by a restart. Nothing depends on someone remembering to check.
- Email Routing Status. A committee-facing status page shows, per role, exactly how mail is routed, whether the destination is verified, and — for the Brevo re-send path — whether the SMTP credentials it needs are actually set, warning before a gap becomes a bounce.
Deliverability, in other words, is treated as a system property with monitoring and self-healing, not a configuration someone got right once and hoped would stay that way.
Related reading
- Email at Scale: Outbound via Brevo, Inbound via Cloudflare — the transport architecture beneath everything here.
- Setting up a Gmail inbox for committee replies and Setting up direct-to-inbox for committee replies — the shared-inbox setup guides.
- Brevo Member Sync: How the Information Flow Changes — how member and consent data moves between NGX, Brevo and Salesforce.
- Per-group instances: the assessment and recommendation — the analysis that settled on a separate instance per group, over a single shared instance, on isolation and blast-radius grounds, which is exactly what makes per-group reputation isolation possible — and the NGX Ramblers Architecture Overview.