> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR-021 — Multi-Program from Day One

> DEC-H2 decided that multi-program would be schema-only in F1: program_id on every loyalty.

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.
