#2013 feat(nurture): suppression — opt-out, address-grain frequency caps, one-click unsubscribe
Opened by keeb · 9/4/2026
Parent: the product-nurture epic. Blocks every send.
Problem
Three separate holes, and all three have to be closed before the first nurture message leaves:
- There is no per-operative product-email preference. The only opt-out that
exists is
newsletter_subscriber.unsubscribedAt, which covers the marketing list. An operative has no way to say "stop sending me lifecycle mail". - Resend suppression does not apply to what we are about to do. That
unsubscribedAtis mirrored back from Resend broadcasts via/api/resend/webhook.ResendMailer.sendPOSTs/emailsdirectly — a transactional path Resend does not suppress. Sending nurture through it silently mails people who have already unsubscribed. - One human can hold several operatives.
accountLinking.disableImplicitLinkingistrue, so a social sign-in whose email does not match mints a new account. Per-operative dedupe is correct and still mails the same person twice. The Attio person grain (#1665) resolved the identical problem by keying on the address.
What ships
A nurture preference on the operative, defaulting to opted-in, with an
/settings-reachable control and an unsubscribe route that works from a mail
footer without a session (signed token, one click, no login wall).
One suppression check, evaluated at delivery time, returning a reason, not a boolean — the panel renders it:
- explicit nurture opt-out
newsletter_subscriber.unsubscribedAtset for this address (we honour it even though Resend will not enforce it here)- banned account
- address-grain frequency cap — at most one nurture email per address per N days (propose 14), across all flows and all accounts sharing that address
- account has no username yet — hold, do not send, the same rule the announce path applies to an unnamed operative
Recomputed at delivery, never at identification, so a preference change between the projector run and the send is honoured — the same level-triggered shape the Attio and Resend queues use.
Every mail carries the footer link. No exceptions, no per-flow opt-out from the opt-out.
Verification
tests/app/nurture_suppression_test.ts— one case per reason, plus the two-accounts-one-address case asserting the second is capped.tests/routes/nurture_unsubscribe_test.ts— a valid token unsubscribes with no session; a forged or expired one 400s and changes nothing.- The panel shows suppressed rows with their reason, which is how this is verified against the real population before anything is armed.
Out of scope
The transport itself (B2) and the copy (B3).
In Progress
Click a lifecycle step above to view its details.