HIP-1220: Commerce — The Merchant Half
Abstract
/v1/commerce is selling: checkout, subscriptions, invoices, spend alerts,
payment webhooks, the storefront and its catalog, carts, priced SKUs and the
typed payment endpoint. It is hanzoai/cloud apps/commerce, which mounts the
hanzoai/commerce module natively on the cloud's own router — one router, one
specificity space, zero handler adaptation (apps/commerce/mount.go:3-14).
This HIP states the target surface — one root for every merchant noun — and the
boundary with billing, the customer money endpoint HIP-0018 declares: commerce is
the merchant half and the store it owns; billing is the address a customer's
money questions are answered at.
Motivation
The app answers at eight top-level roots today (manifest/apps.go:116), which
puts a cart, a storefront and a payment in three different generated client
classes for one subsystem. HIP-0139 §7.1's default closes that: an app with one
store is one capability, however many nouns it answers for.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 One root
Every route commerce serves MUST be under /v1/commerce:
/v1/commerce/cart— the shopper's basket (from/v1/cart; HIP-1220)./v1/commerce/catalog— the merchant catalog rows in commerce's store, merging the SuperAdmin CRUD now at/v1/catalog/*./v1/commerce/payments— the typed payment endpoint (from/v1/payments; HIP-1220)./v1/commerce/plans— plan ROWS in commerce's datastore (from/v1/plans/{entries,seed}). The/v1/plansroot is the plans capability's; these rows are not that catalog and take commerce's prefix./v1/commerce/store— storefronts, listings, checkout (from/v1/store; HIP-1220).- the tenant-admin surface, from
/_/commerce— HIP-0139 §3.3, nothing outside/v1; same audience, same binary. This half of the fold lands in thehanzoai/commercemodule, which registers those routes (one module release).
Where the router still serves the old spellings, the pairs are the commerce
lines in cloud's openapi/misfiled.txt, and each closes by fold, never alias.
§2 The billing boundary
Commerce MUST NOT serve /v1/billing. That address is billing's (HIP-0018),
answered over the plane; commerce keeps the store and publishes the plane
operations — balance, the prepaid gate, credit, usage, transactions, spend and
scope rules (apps/commerce/mount.go:204-210) — that the money endpoint and every
metered surface debit through. One store, one publisher, one address that is
somebody else's.
§3 The store it owns
Per-tenant merchant and money stores under <DataDir>/commerce
(apps/commerce/mount.go:319-323), encrypted under the process master key when
the sqlcipher codec is linked. The at-rest posture is decided in exactly one
function (apps/commerce/mount.go:161-170): a production build refuses to open
money data unencrypted, and a pure-Go dev build gets the module's documented
unencrypted dev store rather than no money plane at all.
The customer ledger of record is NOT here: EmbedConfig.Ledger injects
apps/finance, so a credit minted by a settled charge lands in the one ledger
(apps/commerce/mount.go:6-8).
§4 Tenant
The paying org is read from the validated principal (HIP-0026) and never from a
request field — a field is caller-supplied, and an org read from one is a
cross-tenant write the caller asserted for itself
(apps/commerce/payments.go:25-30). A card payment taken here can only credit
the caller's own org.
§5 Price, and the screen on the mint
The surface is free, in those words: Price: cloud.Free
(plugin/commerce/main.go:21) — it is the path to payment itself. The meter
downstream of the edge is the one §2 publishes.
Both endpoints that mint spendable balance from a settled charge — the browser
top-up and the typed payment op — MUST wrap their handler, not their route, with
the one risk screen (apps/commerce/mount.go, the credit-screen note;
apps/commerce/risk.go): a typed op is projected four ways and only the handler
is the point all four run through.
§6 Events and observability
No commerce.<noun>.<verb> events reach the platform bus, so a customer's
webhooks (HIP-1310) receive nothing from it directly. A settled charge is
stated once, as the analytics event order_completed through the same capture
core POST /v1/event reaches (apps/commerce/emit.go:24-31, :58, :80) —
counted by the same lenses, forwarded by the same destinations fan-out. The
emit is detached and bounded: a conversion row is expendable, a settled payment
is not. Beyond the request span every route gets, it emits nothing.
§7 Typed and declared
The operations this repo owns are typed: payments, invoices, cart, the health
probe and the plane ops. The embedded module's surface is declared with prose
beside the live route (apps/commerce/describe.go:3-14), which cannot add an
operation, only explain one that exists. Each declared route is a typing the
module still owes — the honest typed op exports a value-taking core from the
module first — not a route that can never be typed
(apps/commerce/mount.go, the module-handler note).
§8 Stage and upstream
The stage is ga — the manifest row declares none, and absent is ga
(HIP-0139 §8).
It derives from hanzoai/commerce (pinned v1.50.58 in cloud's go.mod:18),
dual-licensed MIT / Apache-2.0. The whole module survives in HEAD — routes,
datastore, middleware — embedded in-process. PAN-touching paths relay to the
out-of-process Payments/Vault; this binary holds tokens and intent ids only
and is not in PCI-DSS scope (apps/commerce/mount.go:16-19).
Rationale
The alternative to the fold is a split into cart, store, payments and catalog capabilities. All of them read and write commerce's one datastore, so the split is four apps on one store — the defect HIP-0139 §7.2 refuses by name. The nouns stay; they become path segments of the app that owns their rows.
Security Considerations
The wrong implementation hands an attacker the mint. A screen mounted on the router instead of the handler leaves the same authorized deposit reachable unscreened through the tool projection; an org taken from a request field turns a card payment into a cross-tenant credit; a per-config at-rest posture lets a production build quietly write plaintext money data where the per-build decision in §3 refuses to boot.
References
- HIP-0018 — Payment Processing Standard
- HIP-0026 — Identity and Access Management
- HIP-0106 — Hanzo Plugin Contract
- HIP-1220 — Cart · HIP-1220 — Payments · HIP-1220 — Store
Copyright
Released under CC0 1.0 Universal Public Domain Dedication.