15 — Engagement signal & marketing-send flow
15 — Engagement signal & marketing-send flow
Section titled “15 — Engagement signal & marketing-send flow”Status: Draft
Date: 2026-06-20
Owner: engagement + legal (15 engagement-marketing)
Related spec: docs/superpowers/specs/2026-05-23-15-engagement-marketing-design.md §2–§4
Purpose
Section titled “Purpose”Single end-to-end view of how an engagement/marketing contact is produced and
delivered: which producer emits which signal, where the consent gate sits, how
the notify-queue fans out to push vs email, and where the sealed-bid +
double-opt-in (DOI) + frequency invariants are enforced. This is the
design-level companion to spec 15 §3 (“Cross-layer data flow”) and §2 (the
seven layers); it does not introduce new contracts — the binding source is
the spec.
Producer disposition (spec 15 §2.1)
Section titled “Producer disposition (spec 15 §2.1)”| Signal | Producer | Transport | Class |
|---|---|---|---|
bid.outbid | AuctionDO (rank-1 loss) | DIRECT /push/send, one push per (auction, displaced user) — D49 | transactional |
rank.climbed_to_top | AuctionDO (#1 boundary, at-most-once via DO marker) | notify-queue → push dispatch | transactional-adjacent |
auction.closing_soon | active-bidder cron lane | DIRECT /push/send | transactional |
auction.opened | daily opener cron (today only) | notify-queue → consent gate → channel | MARKETING (DOI) |
calendar.day_of_interest | daily calendar cron (flag + consent) | notify-queue → consent gate → channel | MARKETING (DOI) |
won / lost / this_close | saga bestEffortPush | DIRECT /push/send (won/lost rate-cap-exempt) | must-deliver |
| admin campaign | mkt-dispatch hourly cron | mkt_campaign_sends claim → notify-queue | MARKETING (DOI) |
Direct-seam transactional signals never traverse engagement orchestration or frequency-capping (spec 15 §2.5, guardrail 11).
Sequence — a marketing send (auction.opened / calendar / campaign)
Section titled “Sequence — a marketing send (auction.opened / calendar / campaign)”Bounce / suppression backstop (spec 15 §2.4 B0, D56)
Section titled “Bounce / suppression backstop (spec 15 §2.4 B0, D56)”Invariants enforced along this path
Section titled “Invariants enforced along this path”- Sealed-bid (§5):
.strict()zod at the queue boundary; per-template var allow-list ({date},{day},{holiday_label},{rank}∈ {1,2,3} own coarse rank only); the campaign grammar rejectstopic=closing_soon; the ledger has no amount/rank/competitor column and a schema test forbids any JOIN to amount-bearing surfaces. - DOI / UWG §7 (§6): no non-transactional email before
confirmedconsent + logged proof + working unsubscribe;resolveContactGatefail-closes; the DOI confirmation email rides the transactionalapps/api/emails.tsseam, not this queue. - Best-effort everywhere (§2.0): every emit / ledger write is
.catch()-swallowed or deadline-bounded; it never fails the bid hot path or the saga critical path. - Frequency subordination (§2.5): Orchestration owns its cap before
/push/send, so a would-be-capped engagement push is never sent (not lost for 24h); transactional outbid/closing can never be starved.
Failure modes & recovery
Section titled “Failure modes & recovery”- Poison signal (smuggled amount / rank>3 / unknown member) →
.parse()throws → message dead-letters loudly; never silently acked. - Resend transport throw → consumer retries; typed “no” (suppressed / capped) acks as terminal-for-day, not retried.
- Export bundle not yet returning consent rows (D52) → the marketing email channel stays hard-blocked in production; push + transactional email ship first.