docs(engagement): Phase 9 as built — deliverability, suppression and bounces #191

Merged
whitlocktech merged 1 commits from docs/engagement-deliverability into edge 2026-08-31 15:53:12 +00:00
Member

Records what Phase 9 shipped (RunicGateway/website#176) and what building it settled. Operator docs: RunicGateway/runicgateway.com#25

ENGAGEMENT.md

Phase 9 "As built", covering the four decisions taken before any code, the defect the phase exists to have avoided, and the seven-rung live-rig walk.

Two corrections to the plan's own text:

  • "SMTP has none" is too strong. SMTP has no asynchronous bounce or complaint feed, but a single-recipient send refused at RCPT TO throws synchronously with the reply code intact — the highest-value deliverability signal there is, and mailer.js was already catching it and discarding it. So the phase reads it and ships no API transport.
  • PERMANENT_CODES is not a bounce classifier. It answers "is retrying pointless?" and contains EAUTH and 554. Suppressing on it would have emptied the mailing list the first time an SMTP password expired — silently, with a clean send log.

§4.5's DDL gains address_masked and created_by, with the reasoning: a hash-only table cannot be operated, the domain survives so a domain-wide failure is visible, and the local part is destroyed rather than shortened so the column can never be read back as an address book.

§7.1 Q1's narrower half is now answered — an unverified address is excluded at enqueue, and only from the email channel, because a rule spanning channels must still reach that person's in-app inbox.

Also the one thing only the live rig could find: engagement_sends has carried a bounced status since §4.5 and nothing had ever written it, so the Send Log's "Bounced" filter matched nothing and always would have.

BACKEND_DESIGN.md

  • §3 gains engagement_suppressions — including why writes are INSERT IGNORE (the first reason an address was suppressed is the one that survives; an upsert would let a later manual re-add overwrite a bounce diagnosis) and why created_by being NULL is what separates an automatic row from an admin's.
  • §7 gains a Deliverability subsection: the two mechanisms and why they sit at different points in the pipeline, the classifier's rules, the statuses, and the three routes.

Note for the reviewer

BACKEND_DESIGN.md is stored CRLF in this repo while this machine has core.autocrlf=true, which silently rewrote every line on the first git add. Re-staged with -c core.autocrlf=false, so the diff is 227 insertions / 9 deletions rather than 1939/1721.

AI disclosure

Written with Claude Code (Claude Opus 5); the commit carries Co-Authored-By: Claude.

Records what **Phase 9** shipped (RunicGateway/website#176) and what building it settled. Operator docs: RunicGateway/runicgateway.com#25 ## `ENGAGEMENT.md` **Phase 9 "As built"**, covering the four decisions taken before any code, the defect the phase exists to have avoided, and the seven-rung live-rig walk. Two corrections to the plan's own text: - **"SMTP has none" is too strong.** SMTP has no *asynchronous* bounce or complaint feed, but a single-recipient send refused at `RCPT TO` throws **synchronously** with the reply code intact — the highest-value deliverability signal there is, and `mailer.js` was already catching it and discarding it. So the phase reads it and ships no API transport. - **`PERMANENT_CODES` is not a bounce classifier.** It answers *"is retrying pointless?"* and contains `EAUTH` and `554`. Suppressing on it would have emptied the mailing list the first time an SMTP password expired — silently, with a clean send log. **§4.5's DDL** gains `address_masked` and `created_by`, with the reasoning: a hash-only table cannot be operated, the domain survives so a domain-wide failure is visible, and the local part is destroyed rather than shortened so the column can never be read back as an address book. **§7.1 Q1's narrower half is now answered** — an unverified address is excluded at **enqueue**, and **only from the email channel**, because a rule spanning channels must still reach that person's in-app inbox. Also the one thing only the live rig could find: `engagement_sends` has carried a **`bounced`** status since §4.5 and nothing had ever written it, so the Send Log's "Bounced" filter matched nothing and always would have. ## `BACKEND_DESIGN.md` - **§3** gains `engagement_suppressions` — including why writes are `INSERT IGNORE` (the *first* reason an address was suppressed is the one that survives; an upsert would let a later manual re-add overwrite a `bounce` diagnosis) and why `created_by` being NULL is what separates an automatic row from an admin's. - **§7** gains a *Deliverability* subsection: the two mechanisms and why they sit at different points in the pipeline, the classifier's rules, the statuses, and the three routes. ## Note for the reviewer `BACKEND_DESIGN.md` is stored **CRLF** in this repo while this machine has `core.autocrlf=true`, which silently rewrote every line on the first `git add`. Re-staged with `-c core.autocrlf=false`, so the diff is **227 insertions / 9 deletions** rather than 1939/1721. ## AI disclosure Written with **Claude Code** (Claude Opus 5); the commit carries `Co-Authored-By: Claude`.
wtclaude added 1 commit 2026-08-31 15:50:25 +00:00
Records what Phase 9 shipped (website#176) and the four decisions the org lead
settled before any of it: mechanism plus SMTP's own synchronous refusal rather
than an API transport; suppression scoped to engagement rules only; an
`address_masked` column added to §4.5's DDL; and the verification gate applied at
enqueue rather than at delivery.

The correction the phase's own text needed: "SMTP has none" is too strong. SMTP
has no asynchronous bounce feed, but a single-recipient send refused at RCPT TO
throws synchronously with the reply code intact, and mailer.js was already
catching that and discarding it.

The defect worth not repeating: `PERMANENT_CODES` is not a bounce classifier. It
answers "is retrying pointless?" and contains EAUTH, so suppressing on it would
have emptied the mailing list the first time an SMTP password expired.

Also records the one thing only the live rig could find — `engagement_sends`
has carried a `bounced` status since §4.5 and nothing had ever written it, so the
Send Log's Bounced filter matched nothing — and answers §7.1 Q1's narrower half.

- ENGAGEMENT.md: Phase 9 "As built", the §4.5 DDL, Q1's narrower half, status header
- BACKEND_DESIGN.md: §3 `engagement_suppressions`, and §7's Deliverability section

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit 33c0d71e4a into edge 2026-08-31 15:53:12 +00:00
whitlocktech deleted branch docs/engagement-deliverability 2026-08-31 15:53:13 +00:00
Sign in to join this conversation.
No description provided.