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:

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:

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:

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.

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