HIP-1136: Marketing — Lifecycle Email
Abstract
/v1/marketing is lifecycle email: drip sequences that reach the right people.
Audiences resolve from the IAM roster, promo codes record redemptions an admin
grants against, and every send passes through the per-org suppression list. It
is implemented in hanzoai/cloud at apps/marketing (HIP-0106). The two
invariants this HIP states: there is exactly one send seam, and this capability
can no longer mint money.
Motivation
A marketing surface accumulates senders — a campaign blast here, a drip step
there, a calendar hook — and every sender that bypasses the opt-out list is a
compliance violation waiting on a query. The design forces every delivery
through one function so the suppression check cannot be skipped by
construction (apps/marketing/suppress.go:26-32). The second lesson was paid
for: an earlier promo redemption deposited real spendable wallet credit on any
validated principal's say-so — plan and seat count from the request body,
nothing collecting the charge the discount was against — a self-service money
mint worth about $1.79M at the cap. The deposit is gone
(apps/marketing/promos.go:30-48).
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 The store, and the engine underneath
One encrypted SQLite file, the deployment's own marketing
(apps/marketing/store.go:42); every query filters WHERE org=?. Drip steps
are durable tasks on the embedded hanzoai/tasks engine: each enrollment's
next-run time lives in SQLite, a per-minute schedule sweeps due steps, and
every step is claimed once, so a redeploy neither loses nor duplicates a send
(apps/marketing/marketing.go:15-20).
§2 One send seam
Every marketing delivery — campaign, drip step, calendar email — calls the one
deliver function, which consults the per-org suppression list and then hands
off to the platform notify rail; marketing never constructs a provider and has
no second egress endpoint (apps/marketing/suppress.go:138-148). A suppressed
recipient is refused at the seam, and a signed public one-click unsubscribe
writes the same list. A product announcement is not a feature beside this: it
is a one-step sequence with an audience enrolled, inheriting claimed-once
delivery, the suppression gate and the unsubscribe footer
(apps/marketing/marketing.go:42-45).
§3 Audiences are honest
An audience resolves to real mailboxes through the IAM roster; one with an
event filter narrows that roster to the cohort the analytics warehouse
selected. When the roster or warehouse cannot be read the answer is honestly
empty — never a fabricated count, never a send to nobody reported as success
(apps/marketing/marketing.go:18-23).
§4 A redemption is evidence, not money
What a promo redemption produces is a ROW — the org, the server-derived plan,
the discount claimed, and when — which an admin grants against through the
admin surface and the auditable ledger. The plan is derived from the org's live
paid subscription and never accepted from the caller (the input carries no plan
and no seats, so there is no field to inflate); an unreadable plan authority
refuses, inverting the spend gate's fail-open, because failing open here would
let an outage manufacture the evidence money is granted against; the payment
instrument is required as the anti-farming key
(apps/marketing/promos.go:39-60).
§5 The address, tenancy, money, events, observability
Thirty-five operations under /v1/marketing, every one a typed op — one
registry entry projecting REST, the document, the MCP tool and the CLI
(apps/marketing/marketing.go:47-52). The tenant is the org minted from the
validated bearer (HIP-0026), carried to the typed seam by the bridge and read
back server-side — never a header, never an In field. Free (cloud.Free,
plugin/marketing/main.go); the send rail and any granted credit are other
planes' ledgers. It publishes nothing on the bus and emits nothing beyond the
request span every route gets.
§6 Stage and upstream
beta: a vertical application. It derives from no third-party upstream; the
durable engine it composes is hanzoai/tasks (HIP-1062), embedded, not
mirrored here.
Rationale
The alternative to one send seam is a suppression check in every handler, which is the same check N times until one path forgets it. The alternative to deleting the promo deposit was gating it harder — but an automatic path that creates money is not a feature to fix, it is a mechanism to remove, because a money mint left switched off is one flag away from switched on.
Security Considerations
The wrong implementation here is a spam cannon with a ledger attached. Send authority: any path around the deliver seam bypasses opt-out, so the seam is the only egress and tests assert exactly which recipients reach the rail. Money: the removed deposit is the standing lesson — a redemption input that named its own plan converted open signup into self-served credit; the current shape has no field to inflate and fails closed on an unreadable authority. Tenancy: the suppression list, audiences and enrollments are all org-column scoped on the validated principal, so one tenant can neither read another's roster nor unsubscribe another's customers.
References
- HIP-0026 — Identity and Access Management
- HIP-0106 — Hanzo Plugin Contract
- HIP-0139 — Capability
- HIP-1062 — Tasks — The Durable Run
Copyright
Released under CC0 1.0 Universal Public Domain Dedication.