Skip to main content
Status: Proposed · Date: 2026-08-17 · Supersedes: DEC-H2 · Refs: OQ-LOY-07, ADR-011, ADR-016

Context

DEC-H2 decided that multi-program would be schema-only in F1: program_id on every loyalty table from the first migration, a default program auto-created and invisible to the v1 UI and API, and the ability to create more programs enabled later as pure UI work. The reasoning at the time was console complexity. Every screen, every rule, every report gains a program selector, and no design partner had asked for it. Two things changed that assessment, and both came from the owner:
  1. Demand is likely on day one, not later. A holding with two brands, a company running a B2C programme alongside a B2B one for distributors, or a seasonal campaign that must not contaminate the main programme are ordinary situations, not edge cases. Discovering that on the first sales call and answering “later” is a lost deal.
  2. It is a pricing lever, not just a feature. Programme count is a natural, honest plan dimension: the base plan includes one, and needing a second is a concrete reason to move up a tier. A capability that is invisible cannot be sold; one that is visible and limited creates the upgrade conversation on its own.
The deciding argument is asymmetry of regret. The schema cost is already paid — program_id is there either way. What DEC-H2 deferred was console and API surface, and that surface only gets more expensive to retrofit as screens accumulate. Building the selector into six screens now is cheaper than retrofitting it into thirty later.

Decision

Multi-program ships as a first-class capability in F1.
  • A programme is a tenant-visible, named entity from the first release. Its name is set by the tenant, not by us.
  • A tenant may create, rename, activate and deactivate programmes through the console and the Management API, subject to an entitlement.
  • program_id is exposed in the public API v1. Where a request omits it, the tenant’s default programme resolves — so a single-programme integration never has to know the concept exists, while a multi-programme one addresses programmes explicitly.
  • Every tenant still gets a default programme created automatically at provisioning, and exactly one programme per tenant carries is_default = true.
  • Programme count is an entitlement, not a metered metric: the plan states how many are included, and exceeding it is blocked rather than billed as overage. A hard limit is the correct shape for a capability the tenant chooses to adopt, not one their customers’ behaviour drives.
  • Cross-programme aggregation in reporting is explicitly out of scope for F1. Each programme reports on itself. Aggregating across programmes raises real questions — whose points, which currency, which tier ladder — that deserve their own decision.
  • A contact may hold balances, tiers and memberships in more than one programme simultaneously. Points, tiers and rewards never cross programme boundaries (ADR-011 unchanged).

Consequences

+ The commercial model gains an honest upgrade dimension that costs nothing to meter and is easy for a customer to understand. + The domain model matches what the console shows, so there is no period where the API hides a concept the schema already has — the class of bug where a “hidden” identifier leaks through an error message or an export simply cannot occur. + No migration is ever required to enable it, since the schema was already correct. Console surface grows immediately: a programme selector in loyalty screens, and programme scoping in every loyalty report. Estimated at the level of one additional feature spec of work, absorbed inside the console’s own delivery. Every loyalty endpoint must resolve a programme, and every loyalty test gains a multi-programme case. The default-resolution rule keeps the common path unchanged. Support conversations get one more dimension (“which programme?”). Mitigated by the default programme being invisible in practice for single-programme tenants.

Follow-up work

  • FS-LOY-0001 rewritten: programme CRUD, tenant-visible naming, entitlement check, program_id exposed in the API.
  • PRD-LOY: multi-program removed from non-goals; programme count added to the commercial shape.
  • ADR-016: programme count declared as an entitlement dimension alongside the metered metrics.
  • Console: programme selector, as part of its own feature spec.

Alternatives considered

Keep DEC-H2 as written — cheapest now, and the schema cost was already sunk. Lost because the deferred cost is console surface, which grows monotonically; deferring it makes it strictly more expensive, never less. Visible and named, but single programme per tenant in v1 — the middle option. It fixes the “hidden concept” awkwardness for almost no cost, but leaves the commercial lever unbuilt and still requires the selector work later. It optimises for a saving that ADR-021 shows is temporary.