Per Audit Coverage, which names this file. Part of the module spec.
The bounded human bindings register the internal administration requests separately from Seed requests. Native transport and replay consumers retain their own classifications below. Phase 03 owns packet state and proofs.
P02b-1 Step 3 producer implemented — 2026-10-12. Completed provisioning adds one pending outbox row before the existing two MUST rows and commit. Its domain translator adds no independent intent or audit operation and writes no aggregate. A failed MUST write rolls back tenant, organization and outbox together. The integration-event catalogue owns the flow; P02b-2 implements hosted producer dispatch and production-census enforcement. All four steps completed both independent review rounds and local closeout. The merge closeout owns CI/merge status. P02b-3 Steps 1–5 implement durable subscriber delivery and both real defaults; its Step 6 record owns current enforcement and review status.
ADR-0046 producer terminal writer implemented — 2026-10-12; both review rounds complete. The infrastructure operations below belong to this catalogue/matrix, without a synthetic aggregate. P02b-2 Step 2 registers the delivered producer terminal operation; P02b-3 Step 4 adds the subscriber writer and registration, with implementation validation and independent reviews recorded in its delivery record. Both use the restricted fifth audit seam on the fenced state-transition transaction. Routine confined dispatch has the explicit ADR-0046 exemption; platform-scope entry auditing is unchanged.
P02d-4 Step 2 implemented — 2026-10-03. Both review rounds and focused fix verification pass. Site bootstrap is a marked GET/HEAD request, audit Off and without a permission key or grant. It uses matching host scope and READ ONLY; normal reads add no audit row. Public configuration/settings ports remain internal and unrouted. Independent rejected- assertion auditing remains unchanged. The public contract owns allowlisting and lifecycle/locale checks.
P02d-3 Step 1 implemented — 2026-10-02; both review rounds passed.
ITenantSettingsAccessor is an internal application interface, not a
MediatR request or audited write. It creates no intent or business-state mutation.
Any production request introduced for it must be audit Off; test-only requests
stay test-only. The module contract owns the read.
P02d-2 Step 2 implemented; both review rounds passed — 2026-10-02.
The accepted writer
contract implements the locale
and setting rows below. Locale commands declare tenancy.locale.write over the owning
Tenant root and captured locale navigation; branding declares tenancy.setting.write
over TenantSetting. Generic setting values require whole-value [PiiSensitive]
redaction, verified at capture and durable audit. Public branding projection is a
separate allowlist. Verification request types are Off.
Packet 7 ships four audited root writes through three commands: Tenant and default
Organization provisioning, a separate Organization create, and host mapping creation.
P02d-2 adds two locale commands (SHOULD over Tenant) and one branding command (MUST over
TenantSetting). P02b-2 Step 2 adds the producer terminal MUST security-event writer;
P02b-3 Step 4 adds its independent subscriber terminal counterpart.
Rows without a writer remain (planned); operations without a MediatR request
carry (off-path). The claim-context assertion row below is registered off-path
through TenancyAuditCatalogSource.
All four original writes are MUST. Their records commit with the business state, under ADR-0033. The branding writer shares that guarantee; a failed MUST audit rolls its setting write back too.
One command, two rows. An intent is parked per audited (resource, operation)
and not per request (ADR-0044 § 3), so
ProvisionTenantCommand declares tenancy.tenant.create and
tenancy.organization.create, and the owning unit-of-work frame flushes both on the
one transaction. Under the singular reading ADR-0044 retires, the Organization
create row this matrix requires would never be written. Both rows carry the tenant
the transaction announced — for provisioning that is
IProvisionsTenant.ProvisioningTenantId, which is the only value the row's own
WITH CHECK accepts. The value travels on the intent — AuditIntent carries
TenantId and OrganizationId?, resolved by AuditLogBehavior at step 3, because the
store composes the row from the intent and never resolves a tenant of its own
(ADR-0044 Amendment 3).
All four write MUST rows, and the host mapping is the one that matters most: it
is described in the matrix below as "the row that decides whose data an anonymous
request sees", and its row is what records who pointed a hostname where.
TransactionBehavior writes them through IAuditStore.WritePendingAsync immediately
before the commit, since Packet 9.
The snapshots come with them. PlatformHostMapping, TenantLocale,
TenantFeatureFlag, PlatformEntitlement and PlatformKillswitch are plain classes
rather than AuditableEntity<> descendants — Tenancy's share of the plain classes
Audit Coverage § Required Behaviours
lists — and AuditChangeTrackerInterceptor captures every
ChangeTracker entry in state Added, Modified or Deleted minus a named
exclusion list (ADR-0044 § 7) — so the
row this file calls the most important one gets a real before and after rather
than two empty objects.
This matrix is not the floor — Audit Coverage § Baseline Coverage
is, and a module matrix "cannot remove anything in this list". This file adds rows
beneath that baseline and classifies what the baseline leaves open; a tenant
AuditConfig may then narrow SHOULD/MAY at runtime. Neither touches a baseline
MUST.
The Operation column carries the catalogue key — {module}.{resource}.{verb}, the
shape of a permission key with a verb this matrix names rather than the closed action
set (ADR-0044 § 6). Its first two
segments match the permission key for the same resource, so the resource segment drops
the Tenant prefix the module segment already carries — TenantFeatureFlag is
tenancy.feature_flag.* in Permission Standards,
and platform_host_to_tenant keeps the hostmapping segment
permissions.md already gives it. tenancy.tenant_assertion.* is the
one pair that keeps the prefix: nothing in permissions.md gates a
detector the middleware runs on its own, so there is no key for the slug to agree with,
and both are fixed by
ADR-0036 Amendment 7.
One slug per cell, one row per audited (resource, operation). The column is read
as a column of slugs and not parsed as prose
(Audit Coverage § Classification Matrix Template),
so two acts that differ get two rows even where one sentence would explain both.
hostmapping is the one segment here that does not spell a two-word resource in
snake_case, and it is inherited rather than chosen: tenancy.hostmapping.admin is
already the permission key, and the first two segments of the two keys have to agree —
so the spelling changes in both files or in neither.
A cell carries its slug and, where the row is not joined by request type, every marker
that applies — a row can carry both
(ADR-0044 Amendment 3). (planned) is a row
classified ahead of the command that will raise it — the classification
Audit Coverage asks for before the command
ships, which is what makes MUST/SHOULD/MAY a functional distinction rather than a
documentation one. (off-path) is an operation no MediatR request raises, so its
catalogue entry is registered by slug and there is no request type to key it on. An
unmarked row is one a shipped command already raises.
| Resource | Operation | Class | Why |
|---|---|---|---|
outbox_messages |
platform.outbox.dead_letter (off-path) |
MUST (security-event) |
P02b-2 Step 2's consequential BYPASSRLS terminal transition; state and row commit together through the row-bound fifth audit seam, with stored tenant/org/event identity and SystemActor (ADR-0046) |
| delivery ledger | platform.delivery.dead_letter (off-path) |
MUST (security-event) |
P02b-3 Step 4's subscriber terminal writer binds stored tenant/org/event/subscription, acquisition token, reason and attempts to the actual fenced transition; ledger and MUST audit commit together through the same restricted fifth seam. It receives no causal-envelope or payload SELECT grant |
background_jobs |
platform.job.dead_letter (off-path) |
MUST (security-event) |
P02b-4 Step 5 binds stored tenant/org/job/definition/attempt and the actual old acquisition token to the fenced terminal transition. The sixth audit seam writes on that same transaction; audit failure rolls back terminal state. Validation and both review dispositions belong to its delivery record (ADR-0047) |
Tenant |
tenancy.tenant.create |
MUST | The root of a customer's data; its existence is a contractual fact |
Tenant |
tenancy.tenant.change_status (planned) |
MUST | Suspension withdraws access; an operator must be able to say who and when |
Tenant |
tenancy.tenant.rename (planned) |
SHOULD | Presentational |
Tenant |
tenancy.tenant.adopt_seed_origin |
MUST (security-event) |
Closed AdoptSeedAdministrationOriginCommand advances only its fixed Tenant seed-origin marker; its separate hostmapping MUST intent shares the same transaction. |
Organization |
tenancy.organization.create |
MUST | Adds an isolation surface; every org-scoped row written afterwards is written under it |
Organization |
tenancy.organization.archive |
MUST | Withdraws that surface while every row it isolated stays behind, and the rows themselves say nothing about who archived the organization or when |
Organization |
tenancy.organization.rename |
SHOULD | Presentational |
Tenant default reference |
tenancy.organization.select_default |
MUST (update) |
SelectDefaultOrganizationCommand changes only Tenant's default/version; the selected Active/nondeleted Organization must belong to that Tenant. Resource segment matches the organization-admin sub-operation. |
| Organization summaries | tenancy.organization.list |
MUST (read-sensitive) |
Fresh Tenant organization-admin admission before scope-owned configuration/default-version disclosure. |
| Organization detail | tenancy.organization.read |
MUST (read-sensitive) |
Fresh target read admission before Organization configuration/version disclosure. |
| Original Organization/Tenant default outcome | tenancy.organization.read_receipt |
MUST (read-sensitive) |
Current original-target receipt disclosure authority and fresh committed evidence before replay bytes; no mutation rerun. |
TenantDomain |
tenancy.domain.claim (planned) |
MUST | ux_tenant_domains_host is unique on the host alone, so a claim takes a host no other tenant can claim while it stands |
TenantDomain |
tenancy.domain.verify (planned) |
MUST | Verification is the gate the custom-domain lifecycle opens before a mapping row exists; a wrongly verified domain serves one tenant's content at another's address |
TenantDomain |
tenancy.domain.fail (planned) |
MUST | The negative half of the same lifecycle — the attempt count and the recorded reason are what a support thread reconstructs, and repeated failures against one host are worth seeing |
TenantDomain |
tenancy.domain.soft_delete (planned) |
MUST | ux_tenant_domains_host is partial on deleted_at IS NULL, so retiring the row releases that globally unique host for another tenant to claim |
TenantSetting |
tenancy.setting.write |
MUST | Audit Coverage puts "tenant setting changed" on the Tenancy baseline row; a tenant AuditConfig cannot narrow it |
TenantSetting |
tenancy.setting.soft_delete (planned) |
MUST | Removing a setting is a change to it, so the same baseline row covers it — and ux_tenant_settings_tenant_id_organization_id_key is partial on deleted_at IS NULL, so the key becomes free for a successor |
Tenant |
tenancy.locale.write |
SHOULD | Configuration; contained TenantLocale changes are captured with their owning Tenant root |
TenantFeatureFlag |
tenancy.feature_flag.write (planned) |
MUST | "Feature flag toggled" is on the same baseline row, and Feature Flags § Audit classes both flag surfaces as security events |
platform_killswitches |
tenancy.killswitch.toggle (off-path) (planned) |
MUST | A platform-wide flip that disables a capability for every tenant at once. It is not a tenant_feature_flags row — that table's foreign key to tenants is one the platform sentinel cannot satisfy, so killswitches ship as their own platform-scoped table (ADR-0045 § 5). Every write goes through EnterPlatformAdminScope(reason), which is what gives the row a real actor (Feature Flags § Killswitch Pattern); the scope is entered by a service method and not by a MediatR request, so there is no request type to key a catalogue entry on. Packet 9 ships the table, its policies and the read path and no writer: nothing can enter that scope while DenyAllPlatformAdminGate is the registered gate, so Phase 03 owns the toggle, its permission and its runbook (ADR-0045 Amendment 1) |
platform_entitlement_cache |
tenancy.entitlement.refresh (planned) (off-path) |
MUST | Changes what the tenant may do; written only by IEntitlementProvider.RefreshAsync, a port method driven by the Hub over /api/internal/* (ADR-0045 § 1) and never by a MediatR request, so there is no request type to key a catalogue entry on. Both markers, for the reason tenancy.killswitch.toggle carries both: Packet 9 ships NullEntitlementProvider, which answers from constants and touches no table, and the HubEntitlementProvider that performs a real refresh lands in Phase 02c (ADR-0045 § 6) — so there is no writer here yet either |
platform_host_to_tenant |
tenancy.hostmapping.write |
MUST | The resolution index — the row that decides whose data an anonymous request sees |
platform_host_to_tenant |
tenancy.hostmapping.delete (planned) |
MUST | The same row removed. The table carries no deleted_at, so the delete is a row gone and a host that resolves to nobody |
TenantAssertion |
tenancy.tenant_assertion.reject (off-path) |
MUST for admitted events (security-event) |
A validated header/claim mismatch with an independently defended host tenant is recorded under that host tenant, never the asserted tenant or sentinel; row 14 is counted only. ADR-0065 admits 600 authenticated standalone write attempts per host tenant per API instance per first-request-anchored monotonic minute, shared across scoped recorders, actors, sources and dimensions. Failed writes consume permits; later occurrences make no audit-store call and retain their refusal, count and exactly one attributed Warning (4006 replaces normal 4001). An attacker can exhaust the allowance, so later actors lose durable evidence; aggregate log volume remains unbounded. Admitted events retain MUST failure/health behavior, tenant-wide organization scope, exact actor/correlation and trusted organization in metadata. No tenant AuditConfig override or independent tenant-registry cap exists. General mutation MUST durability is unchanged. Packet 9 owns the recorder/decorator path; P02b-5 Steps 3–4 implement JWT attribution and HTTP activation under ADR-0064. ADR-0065 acceptance precedes the dependent budget implementation; the merge closeout owns its final delivery status |
TenantAssertion |
tenancy.tenant_assertion.anonymous_burst (off-path) |
MUST (security-event) |
The anonymous half of the same detector, written once per (resolved tenant, dimension, window) when the mismatch counter crosses its threshold. An anonymous mismatch is not itself an audited operation; the burst is — the precedent the baseline's "login failure burst" already sets (ADR-0036 Amendment 7). Same writer, and (off-path) for the same reason. The window is in-process and never ICacheService — a cache outage must not decide whether a MUST-class security event is recorded — so the effective threshold is per instance, which errs toward more auditing. Tenancy:AssertionBurst sets it; the defaults are ten occurrences in five minutes |
| Confirmed claim-context assertion | tenancy.assertion.reject_claim_context (off-path) |
MUST (security-event) |
A header conflicting with actual successful ClaimAndMembership resolution uses the confirmed tenant/actor, fixed dimension/reason and membership-confirmed source. No asserted UUID or host allowance. The production claim-context recorder and central evidence owner implement this path; Phase 03 owns qualification. |
| any | platform.admin_scope.enter (off-path) |
MUST (security-event) |
Cross-tenant access is the one read worth a row; Audit Coverage puts every platform-bypass invocation on security-event. Packet 9 ships it: an admitted PlatformAdminScope.EnterAsync writes the row through IAuditStore.WritePlatformScopeAsync on the scope's own platform-role connection, in a transaction of its own that commits before the caller's begins — so work that later fails, throws or is abandoned cannot take the record with it — carrying TenantId.PlatformSentinel (ADR-0044 § 1, § 10). An entry the catalogue cannot classify, or whose row cannot be written, is refused — no transaction is handed out for it. The Warning log line stays beside the row, carrying its id, as the real-time signal the row is not. EnterAsync is a service method and not a MediatR request, and the catalogue is keyed by request type — so its catalogue entry is registered by slug rather than by request type. Its module segment is platform for the same reason: the scope belongs to no module's request path. The permission gate denies before opening a connection; those denied attempts write no entry row. Phase 03 must decide denied-entry evidence before its first privileged consumer |
A feature flag that gates a billed capability is not a tenant_feature_flags
row at all. It is plan-level, it is written only by
IEntitlementProvider.RefreshAsync, and it is audited on the
platform_entitlement_cache refresh row above (tenancy.entitlement.refresh).
The distinction routes the change to the right row; it does not make either row
optional. IFeatureFlags reads the plan half through the provider and the tenant
half from tenant_feature_flags, and no module reads the cache table directly
(ADR-0045 § 2).
TenancyAuditCatalogSource declares the same operations in code through
IAuditCatalogSource.Describe(IAuditCatalogBuilder) rather than parsing this file; the
matrix stays the human-readable artifact, and AuditCoverageTests asserts the two agree
in both directions (ADR-0044 § 6).
That join runs in two directions and they have different domains
(ADR-0044 Amendment 3). Catalogue → matrix
is total: every entry this module's IAuditCatalogSource registers has a row here
carrying the same slug, and one that does not fails. Matrix → catalogue binds only to a
slug whose request type exists, which is why the remaining (planned) rows are
classification and not drift — and a (planned) row whose command has since shipped
fails, so the marker is a claim the rule re-checks on every run rather than an
exemption.
Off-path rows use slug registration when their writer ships. Delivered
registrations are platform entry, producer/subscriber/job terminal transitions,
the two tenancy.tenant_assertion.* operations and
tenancy.assertion.reject_claim_context. The entitlement refresh and killswitch
rows carry both (off-path) and (planned) until their named writers exist.
They have no synthetic request type and sit outside the request-type join in
both directions. Every ordinary request
slug joins in both directions when its command/query lands.
GetTenantSeedStateQuery, GetOrganizationSeedStateQuery,
GetHostMappingSeedStateQuery, GetSettingSeedStateQuery are explicitly Off in
TenancyAuditCatalogSource. These unrouted queries verify announced
tenant state through the ambient transaction and declare no write operation.
GetPublicSiteQuery is explicitly Off in TenancyAuditCatalogSource. Its GET/HEAD
endpoint returns only the public configuration allowlist through the marked
read-only pipeline. No operation slug or resource audit row is registered. Normal
successes and refusals add no intent; independent rejected-assertion auditing is
unchanged. IPublicTenantConfigurationReader and ITenantSettingsAccessor are
internal ports, not separately dispatched or directly routed endpoints.
GetAccountSiteQuery is explicitly Off through
builder.Off<GetAccountSiteQuery>() in TenancyAuditCatalogSource, as required by
ADR-0064.
The non-public query, exposed only by AccountSite GET, returns its own actor/scope
and the existing
public site projection. Normal success and refusal create no query resource intent
or slug; it is neither ReadSensitive nor MAY. Accepted ADR-0077 separately requires a central Identity
denial event for live membership denial attributable to a resolved tenant.
Independent rejected assertions retain their own path. The
module contract owns admission.
ADR-0064 keeps the two existing operation slugs and extends their captured evidence:
TenantAssertionSourcemaps only toheaderorjwt-claimin counters andmetadata.assertionSource. The existing dimension, asserted UUID and authenticated fields remain;metadata.trustedOrganizationIdcarries the trusted UUID or null.- The row takes
ActorUserIdandCorrelationIdfrom the captured rejection, including when resolution refuses before any tenant context is installed. Its tenant is the defended host tenant, andOrganizationIdremains null for tenant-wide visibility. - Row 7's rejected organization produces one authenticated rejection row under the host tenant when admitted by ADR-0065's host-tenant allowance. Row 10 remains unaudited. Separate admitted occurrences remain separate rows. Suppressed occurrences write no row and retain their refusal, mismatch/suppression counters and one Warning.
- Row 14 calls synchronous
RecordNoTenantRefusalwith no tenant or asserted value. The decorator performs no store I/O. Logging increments the existing unresolved counter with onlysource=jwt-claimanddimension=Tenant, and emits the fixed Debug diagnosticmembership_unconfirmed. Actor and correlation are structured log fields, never metric labels; no audit slug or row is added. - Existing anonymous burst limits and failure behavior remain: a failed audit preserves the refusal while the audit store reports Critical, failure count and unhealthy state.
Standard 18 owns the exact evidence population, trusted provenance and durability rules. The new assertion event requires confirmed ClaimAndMembership resolution from the production reader. It cannot use a candidate/raw claim or impersonate a host-backed rejection. Host-backed events keep their own allowance; row 7 stays host-recorded, row 10 unaudited, and refused row 14 diagnostic-only. A successful row 14 followed by conflicting tenant/org header is the distinct new MUST event, without an asserted UUID or host-budget debit. Actual refusal survives audit failure with the existing failure signals. The Phase 03 Status block owns source-bound qualification.
P03-1 Step 2 declares every current Tenancy scalar in its EF mappings. Institution routing, entitlement and definition fields are explicit None with purpose; inherited person references and stamps use the common declaration contract. Setting JSON, free-text verification errors/operator explanations and actor references are classified Identity. This conservative declaration reads no row values and grants no public projection, log exposure or new retention policy. TenantSetting.Value retains its independent whole-value snapshot redaction. The model finalizer and complete census reject undeclared/new neutral fields; provider/domain-verification and privileged toggle writers retain their named phase authority and fixed-diagnostic requirements before activation.
ADR-0093
adds the exact tenant-wide identity.invitation.ttl_days policy. Its setter is a
consumer of the existing MUST tenancy.setting.write operation,
not a second catalogue slug. SetTenantInvitationLifetimeCommand writes one
TenantSetting with tracked whole-value redaction and same-transaction audit;
conditional first creation/replacement cannot silently upsert. Branding retains
its separate typed request and the same established audit operation.
| Resource | Operation | Class | Why |
|---|---|---|---|
| Original invitation-policy TenantSetting outcome | tenancy.setting.read_receipt |
MUST (read-sensitive) |
ReadInvitationPolicyReceiptQuery; original setting ID/key, current exact orgless authority and committed read evidence before protected replay. |
The internal typed accessor creates no separate request/event. The existing
Tenant tenancy.setting.write pair knowingly covers this exact policy beyond
branding. Historical v1 Roles without it cannot edit; default 14 days works
without a new grant. The module contract
owns range/default and expiry freezing; Phase 03 Status
owns implementation and qualification. These rows supply no completed census or
provider/browser proof.
Internal human requests and catalogue bindings implement ADR-0079. Phase 03 owns packet state. Existing slugs may bind more than one request type; the catalogue/matrix join remains total. Human wrappers have separate Protected policies, never Seed provenance or nested dispatch to Seed-only commands. The classification matrix above remains the sole class/type declaration; this request-binding table maps types to those rows:
| New human request | Aggregate subject | Matrix operation |
|---|---|---|
ListOrganizationsQuery |
Organization summaries | tenancy.organization.list |
GetOrganizationDetailQuery |
Exact detached Organization | tenancy.organization.read |
CreateManagedOrganizationCommand |
Organization | tenancy.organization.create |
RenameOrganizationCommand |
Organization | tenancy.organization.rename |
ArchiveOrganizationCommand |
Organization | tenancy.organization.archive |
SelectDefaultOrganizationCommand |
Tenant | tenancy.organization.select_default |
AddManagedTenantLocaleCommand |
Tenant | tenancy.locale.write |
SetManagedDefaultTenantLocaleCommand |
Tenant | tenancy.locale.write |
SetTenantLocaleEnabledCommand |
Tenant | tenancy.locale.write |
RemoveTenantLocaleCommand |
Tenant | tenancy.locale.write |
SetManagedTenantBrandingCommand |
TenantSetting | tenancy.setting.write |
Locale writes are contained Tenant changes: their permission/audit resource is
locale, while snapshots and version belong to Tenant. Default selection is the
Tenant-root sub-operation of tenancy.organization.admin, not a general Tenant
settings grant. Its Tenant intent and returned version describe that root only.
Successful mutation audit shares business commit; existing MUST rollback remains.
AdoptSeedAdministrationOriginCommand is a distinct closed Seed request for one
Tenant. It declares both tenancy.tenant.adopt_seed_origin and existing
tenancy.hostmapping.write MUST intents on its common transaction. Tenant marker
and fixed projection commit together for qualified 0→2 / 1→2; an exact existing
mapping is designated unchanged without an UPDATE. Under Accepted
ADR-0081,
the runner's marker-2/exact-live-mapping branch skips mutation dispatch, witness
minting, the temporary hatch and MUST intents. The separate completion check is
internal to this act, adds no successful sensitive-read event or new Off request,
and retains failed/denied and genuine unknown-COMMIT evidence on refusal.
It grants no human host-mapping permission or v2 Role authority and introduces
no second aggregate/provisioning exception. The
canonical completion contract
owns the separate source, retained completion facts, restoration, marker/repeat
and recovery rules; qualification remains in the phase Status block.
Organization/default protected receipt reads are ordinary MUST sensitive reads with current original-target disclosure admission before stored bytes. Failed guard/audit retains the completed receipt, and original mutation evidence is not replayed. Off global-self/current-context navigation obtains Tenancy labels only through the separately confined own-discovery reader; the helper creates no independent resource intent or general cross-tenant directory. Own-current-Tenant navigation uses the separate invocation-bound ordinary ReadOnly contract and also adds no independent resource intent. AccountSite remains Off under its actual-host ceiling, including the separately validated native-session origin.
GetTenantInvitationPolicyQuery is explicit Off/physical ReadOnly own-
current-tenant navigation in orgless scope. It returns only the fixed policy
value/version/default-presence precondition and advisory canEdit; it grants no
write authority and reads no arbitrary setting. Its Off registration precedes dispatch.