Skip to main content

D71 — DefaultRoleRegistry: default roles are contributed per module (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.

The default-role analogue of the D36 per-module PermissionRegistry. Previously the explicit OPERATOR (web) default roles were hard-coded in the Authorization module's Support\DefaultRoles (backoffice() named them by BackofficePermission enum case; complianceBackoffice() by string) — so the DEFINITIONS of operator/platform-support/platform-compliance + the compliance analyst/officer/manager/ admin ladder lived in Authorization even though their capabilities are OWNED by the Backoffice/Compliance surfaces. That blocked moving the operator-surface permission catalog out of the central enum (the docs/tracking/permission-catalog-migration.md "default-role contribution seam" deferral).

Decision: an Authorization-owned Permissions\DefaultRoleRegistry singleton (append-only register()/all()/forGuard()) that modules PUSH their default-role definitions into from their ServiceProvider::boot() — via Permissions\Contracts\ProvidesDefaultRoles + each module's src/Authorization/DefaultRoleProvider.php, exactly mirroring ProvidesPermissions/PermissionProvider. A Permissions\DefaultRoleDefinition (name, guard, permissionKeys) names its permissions by KEY STRING, never an enum — so a module can define a default role over ANOTHER module's permission catalog without importing it. Backoffice contributes operator/compliance/platform-*; Compliance contributes the compliance-* ladder. BackofficeRoleSeeder + SyncPermissions::validateDefaultRoles() iterate DefaultRoleRegistry->forGuard('web') (composed through RoleComposer, unchanged); DefaultRoles keeps only the full-catalog role names, the explicit TENANT map, and the derived protected role-name lists (backofficeRoleNames() reads the registry, so ProtectedOperatorRoles can't drift).

Why (control inversion, not a graph edge): the modules depend DOWN on Authorization and PUSH into its registry at boot — Authorization never imports Backoffice/Compliance, so the dependency graph stays acyclic (ModuleBoundaryTest unchanged; the registry-inversion precedent of PermissionRegistry, FeatureGuard, the webhooks/dlp registries). The relocation is behaviour-preserving: the seeded role→permission sets are byte-identical to before the seam (snapshot: DefaultRoleRegistryTest; ProtectedOperatorRolesTest pins the protected set). The operator permission KEYS themselves stay in the central BackofficePermission enum for now (referenced by string) — relocating those cases is the remaining, separately-gated catalog-migration work.


← Decision log index