OneShot

All packsTransactional Email Deliverability CoreSolve

email suppression list complaint overwritten by bounce

A complaint is not a bounce that arrived later

A naive suppression upsert lets a later hard-bounce webhook overwrite a spam-complaint row with a weaker reason -- silently un-suppressing a reputation signal.

3 of 3 runs passedOne decision inside Transactional Email Deliverability Core (nextjs-postgres-resend).

What goes wrong

A spam complaint and a hard bounce are both reasons to suppress an address, so it's tempting to store both in the same suppressions table with a plain upsert -- whichever event arrives last wins. If a hard-bounce webhook for the same address arrives after (or is reprocessed after) a complaint, a naive upsert silently downgrades the row's reason from complaint to hard_bounce, erasing a signal that's supposed to be legally and reputationally permanent.

Why agents get it wrong

An upsert (ON CONFLICT DO UPDATE) is the standard, idiomatic way to keep a suppressions table current, and "the most recent event wins" is a reasonable default rule for almost every other field in a system like this. Complaints being a one-way door -- never downgradeable, regardless of what arrives afterward -- is a domain rule that has to be stated explicitly; it doesn't fall out of normal upsert semantics.

What Transactional Email Deliverability Core pre-decides

Suppression decision 2 makes the upsert conditional: ON CONFLICT ... DO UPDATE only when the existing reason is not complaint. Decision 3 backs this up on the read side too -- unsuppress() refuses to remove a complaint-reason row at all, returning {removed: false} and leaving it in place, because a complaint is treated as a reputation signal to preserve, not a typo to fix.

Source: ARCHITECTURE.md 'Suppression semantics', decision 2

Coming soon — $79Full receipt and details for Transactional Email Deliverability Core

Related problems