9.2 KiB
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | depends_on | unblocks | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| TEN-WP-0010 | workplan | Make tenant grouping mutable and the identifier segment historical | infotech | tenant-engine | ready | claude | tenant-guardrails | 2026-08-17 | 2026-08-17 |
|
TEN-WP-0010 - Mutable grouping
tenant:<grouping>:<name> embeds a mutable attribute in an immutable
identifier. Several groupings are headcount bands — small ≤10, medium ≤100,
large ≤1000 — and headcount changes.
Raised by rapp-postgres while researching identifier design, and correctly
handed to us rather than folded into their framework: the identifier format is
NetKingdom canon's (ADR-0013) and the consequences are ours.
Why this is now urgent rather than tidy
Until last week grouping was a classification label and a stale one was a cosmetic problem. TEN-WP-0006 made grouping load-bearing for money. Guardrail spend ceilings resolve from grouping:
| Grouping | spend.monthly |
|---|---|
trial |
0 |
small |
€250 |
enterprise |
€20 000 |
So a frozen grouping now means a tenant onboarded as small keeps a €250
ceiling after growing past a thousand people, and a tenant onboarded as trial
keeps a zero spend ceiling permanently. That is a commercial defect, it is
live in production as of TEN-WP-0007, and it gets more expensive per tenant
onboarded.
Renaming is not the fix and will not be attempted. The identifier is the IAM
Profile tenant claim value that key-cape mints into tokens, it is the key
shared with user-engine, and it appears in OpenBao paths. Renaming is an
estate-wide migration.
The fix, and why it is small
Tenant.grouping is already a separate field on the record, parsed from
the identifier once at creation. Nothing needs to be added to the model and
nothing needs to be renamed. Four changes:
- The identifier's middle segment is historical — onboarding-time, immutable, and explicitly not authoritative for current grouping.
- The grouping field becomes mutable through an audited mutation, in the same shape as the lifecycle operations.
- Guardrail resolution already reads the field, so it picks up a change with no change on the resolution path.
- ADR-0013 states that no consumer may parse grouping from the identifier and treat it as current.
Point 4 is the one with fleet reach, and it is why this cannot be done quietly: any consumer parsing the grouping segment today is reading a value that is about to start going stale, and does not know it.
T01 - Propose the ADR-0013 amendment to NetKingdom canon
id: TEN-WP-0010-T01
status: todo
priority: high
The identifier format is canon's, not ours. Propose rather than decide.
The amendment should state which of the two readings is intended — grouping frozen at onboarding, or grouping as current classification — and we are arguing for the second with the identifier segment demoted to historical. State the guardrail consequence plainly, because it is what makes the choice non-neutral: under the frozen reading, canon is choosing that a growing tenant keeps its onboarding spend ceiling forever, and somebody should choose that knowingly rather than inherit it.
Include the explicit prohibition on parsing grouping from the identifier, and
that parse_tenant_identifier keeps validating the segment at creation —
demoting it to historical does not make it free-form.
Done when the amendment is proposed and its ratification status is recorded here. Implementation may proceed in parallel — the field already exists and making our own record mutable does not require canon's permission — but the prohibition in point 4 is not fleet-binding until ratified.
T02 - Find out who parses the grouping segment
id: TEN-WP-0010-T02
status: todo
priority: high
Survey the estate for consumers that split the identifier and read
parts[1]. Known parsers of the identifier exist by construction — the format
is canon and the string is everywhere.
Candidates worth checking specifically: key-cape (mints the claim),
user-engine (shares the key, has an operator UI that may display a grouping),
flex-auth (policy conditions could match on it), adaptive-pricing (plan
eligibility by grouping would be the obvious use), and OpenBao path
construction.
rapp-postgres is confirmed unaffected — ADR-0001 forbids them from parsing
tenant identifiers.
For each parser found: it is not necessarily wrong to read the segment, but it must be reading it as onboarding-time, not as current. Anything using it for a live decision needs to call us instead.
Done when the list is complete and each parser has either confirmed onboarding-time semantics or been given the read API to use instead.
T03 - Implement the audited grouping mutation
id: TEN-WP-0010-T03
status: todo
priority: high
Add POST /tenants/{tenant_id}/grouping — deliberately not an addition to
MUTABLE_METADATA_FIELDS. display_name and contact_email are cosmetic;
grouping determines spend ceilings. A change that moves money must not travel
the same path as a rename, must not share its authorization action, and must
be visible as its own thing in the audit trail.
It fits the existing machinery: mutate_tenant() already takes a
Callable[[Tenant], Tenant], so this is a new domain method plus a route, not
new store surface.
- validate the target against
GROUPINGS; - reject on reserved identifiers —
tenant:platformandtenant:coulombare ungrouped (grouping is None) and resolve guardrails through the reserved profile; giving them a grouping would silently move them onto the grouping ladder; - reject a no-op, consistent with
with_metadata; - require
Idempotency-Key,If-Match, actor, reason, correlation id; - new flex-auth action
tenant.grouping.set, resource typetenant.
That action is a policy-package change outside this repo — the same tracked
handoff as TEN-WP-0006-T04. It takes tenant-engine.write-api.mutate from nine
actions to ten.
Retired tenants: follow the TEN-WP-0005 reduce-privilege precedent. A
grouping change alters the ceiling, so the same asymmetry applies as for
guardrail overrides — a change that would loosen the resolved ceiling on
reactivation is refused, a tightening one is allowed. guard_guardrail_write
already computes exactly this comparison; reuse it rather than writing a second
notion of "loosening" that can drift from the first.
Done when the mutation is authorized, versioned, idempotent, correlated, and audited, with the reserved-identifier and retired-tenant cases tested.
T04 - Interactions with guardrails and grants
id: TEN-WP-0010-T04
status: todo
priority: high
Two places where grouping is load-bearing beyond the ceiling table.
Guardrails. A grouping change silently changes every grouping-derived
ceiling. Prove: the effective limit follows the new grouping; an existing
per-tenant override still wins (precedence is unchanged); provenance still
reads grouping after the move; and the audit trail lets someone reconstruct
why a ceiling changed on a date when no guardrail write happened. That last one
is the reason this task exists — a ceiling that moves with no guardrail event
against it is exactly the kind of thing that looks like a bug at 2am.
Consider whether the grouping mutation should emit a guardrail-visible event as well as a tenant event, so the ceiling's history is complete from either side.
Grants. create_role_grant restricts platform_default grants to
trial-grouped or reserved tenants (ADR-0014). So a tenant moving off
trial can no longer receive new platform_default grants, while its existing
ones remain — correctly, since the trail is append-only. Test that the existing
grants survive untouched and that the next platform_default attempt is
refused with a comprehensible error rather than an invariant violation.
Done when the interactions are covered in the conformance suites over all backends.
T05 - Ship, and tell the consumers
id: TEN-WP-0010-T05
status: todo
priority: medium
Contract documentation, then the handoff: flex-auth needs
tenant.grouping.set in the policy package; every parser found in T02 needs to
know the segment is historical; adaptive-pricing needs to know grouping can
now change under a tenant if plan eligibility depends on it.
Rollout is a separate operator-authorized step, per TEN-WP-0007. Do not deploy as part of this workplan.
Done when the contract is documented and the notes are sent.
Out of scope
- Renaming tenants. Not now, not as a fallback. The identifier stays immutable.
- Automatic reclassification. Nothing here infers a grouping from headcount or usage. The mutation is deliberate, audited, and authorized — an automatic version would move spend ceilings with no actor to attribute it to.
- Revisiting the ceiling values. The non-
trialdefaults remain conservative openers pending product sign-off (TEN-WP-0006-T01). This workplan makes the grouping correct; whether €250 is right forsmallis a separate question. - Backfill. Every existing tenant's field already matches its identifier segment, so there is nothing to migrate — the two only diverge once someone uses the new mutation.