D73 — Per-tenant entitlement overrides: tenant_feature_entitlements (the 2nd RLS table) + override→level→default precedence under the jurisdiction gate (RESOLVED)
Architecture decision record, relocated verbatim from the retired single-file
docs/tenancy/decision log. Status, thematic clusters, and how to record a new ADR: the decision log index.
subscriptionify-style per-tenant overrides (docs/tracking/feature-entitlement-overrides.md) ship: a
specific tenant can be granted/limited a feature independent of its commercial tier, without a bespoke tier.
Storage — the SECOND tenant-scoped table. tenant_feature_entitlements carries a tenant_id FK → PostgreSQL
RLS (forced policy auto-discovered by tenants:rls; isolation proven by the mandatory
TenantFeatureEntitlementIsolationTest + tenants:verify-isolation), the same enabled + value JSONB shape
as level_feature_entitlements, plus an optional valid_until time-box. A partial unique index
(tenant_id, feature_key) WHERE deleted_at IS NULL allows one LIVE override per feature while a revoked
(soft-deleted) row does not collide (a fresh grant after a revoke opens a new live row). The catalog stays
central; this is the one catalog-side piece that MUST be tenant-scoped (it holds per-tenant data).
Resolution precedence — override → level → catalog default, UNDER the jurisdiction gate. FeatureResolver
still checks the jurisdiction gate FIRST (short-circuit); only then does it prefer a live per-tenant override
over the tier. An override can therefore NEVER grant a jurisdiction-forbidden feature — compliance still beats
commerce (D34). The precedence holds in BOTH resolve() (single) and resolveMany() (batch): the batch path
loads the tenant's overrides in ONE whereIn query (TenantEntitlementRepository::overridesFor, the analogue of
the batched permitted-set/level-rows/usage reads), so the /me abilities feed stays query-bounded (the
AbilitiesResolverTest <10-query guard stays green). Precedence semantics are deliberate: the override read
returns a NON-NULL value even for a force-OFF (enabled = false) — a live force-off row beats a tier that
would enable the feature — and null ONLY when there is no live (non-soft-deleted, non-expired) row.
Grant API + events. Bus-only operator commands GrantFeatureOverrideCommand / RevokeFeatureOverrideCommand
(no route) gate on the Features module's FIRST RBAC permission, features.overrides.manage (web; the module now
owns a PermissionProvider registered into the central registry — the first Features permission), resolve the
tenant by PUBLIC uuid (→ TENANT_NOT_FOUND when unknown), write through TenantEntitlementRepository inside the
tenant's RLS scope, and emit feature.granted / feature.revoked domain events (public uuid payloads). The
repository is a sanctioned out-of-bus model reader/writer (the resolver reads overrides through it), added to the
CqrsBoundaryTest exact-class carve-out beside FeatureUsageRepository.