persona Hierarchy and Scope Model
- Status: proposed
- Deciders: Platform Team
- Date: 2026-07-13
- Parent: 0001 — Adopt WhiskerPlatform over rwe-core
Technical Story: Defining persona (user role hierarchy) and how it
interacts with store-scoped access across whisker-host and modules
Context and Problem Statement
Section titled “Context and Problem Statement”WhiskerPlatform needs a documented persona model to gate pages,
config, and tooling across whisker-host and modules. The working list of
candidate types was:
developer, admin, corporate, globalOps, support, storeOps,
coach, athlete
Two problems surfaced while trying to finalize this list:
- Role and scope are conflated.
storeOps,coach, andathletediffer mainly by how many stores they can see (a scope question), whileadmin,globalOps, andcorporatediffer mainly by which config domains they can touch (a capability question). Encoding both in one flat enum creates pressure to mint a new type every time a scope variant appears (e.g. “storeOps for one region” vs. “storeOps for a district”). - Not every type ranks cleanly against every other type.
globalOpsis a strict subset ofadmin(minus configs gated until an admin approves access). Butsupportandcorporatedo not obviously outrank each other — they operate in different domains at a similar power level. Forcing one total order (developer > admin > globalOps > corporate > support > storeOps > coach > athlete) hides thatsupport’s access is closer in kind todeveloper’s tooling access (logs, feature flags, incident tools) than tocorporate’s store-content workflows — it’s just missing the configs that are deliberately hidden from non-engineering roles.
This ADR proposes a persona set, groups it into bands with an
internal ladder per band, and separates scope (store breadth) and
grants (named exceptions) as independent axes so the type list doesn’t
grow every time a scope or exception variant appears.
Decision Drivers
Section titled “Decision Drivers”- Least privilege / blast radius — infra, secrets, and deploy access
(today folded into
developer) should not share an identity with day-to-day feature work. - Avoid role explosion — new store-subset variants or one-off
approvals should not require a new
persona. - Auditable exceptions — incident-driven or admin-approved access (support tooling, gated globalOps configs) should be visible, revocable grants, not silent differences between accounts of the same type.
- Code simplicity — authorization checks in
whisker-hostand modules should test capabilities (e.g."storeConfig:write"), not scatteredpersona === 'admin'string comparisons. - Org shape — the list should mirror how Nike retail actually organizes people (platform engineering vs. global business admin vs. store-local), not just accumulate types as edge cases appear.
Considered Options
Section titled “Considered Options”Option A — Single flat enum (status quo proposal)
Section titled “Option A — Single flat enum (status quo proposal)”Keep the original 8 types as one list with an implicit total order.
Option B — Role + scope as two independent fields
Section titled “Option B — Role + scope as two independent fields”persona (capability tier) and scope (global |
storeList | store) as separate fields on the user/session; no change
to how many persona values exist.
Option C — Role + scope + banded ladder + named grants (recommended)
Section titled “Option C — Role + scope + banded ladder + named grants (recommended)”Same as B, plus:
- Group
personavalues into bands (platform engineering / global business admin / store-local), each an internal ladder. - Add
platformOwneras a new type, split out fromdeveloper. - Move
supportinto the platform-engineering band (adjacent todeveloper), not ranked againstcorporate. - Represent one-off or approval-gated capabilities (globalOps’s gated
configs, support’s incident-only tooling) as named
grants[]/revokes[], not new types.
Pros and Cons
Section titled “Pros and Cons”Option A — Flat enum
Section titled “Option A — Flat enum”- Good: simplest to reason about initially; matches the original list verbatim.
- Bad: scope variants (region vs. district vs. single store) pressure new
types;
developerremains a single “god” identity covering both feature work and infra/secrets; no clean answer for wheresupportranks relative tocorporate.
Option B — Role + scope split
Section titled “Option B — Role + scope split”- Good: removes most scope-driven pressure to add types (a
storeOpsuser with 3 stores and one with 30 stores are the samepersona). - Bad: does not resolve the
developerblast-radius problem or thesupport/corporate/globalOpsranking ambiguity by itself.
Option C — Banded ladder + grants (recommended)
Section titled “Option C — Banded ladder + grants (recommended)”- Good: resolves both problems above; keeps the type count stable
(9 types) even as new scope shapes or approval workflows appear;
gives incident/security review a clear, auditable place to look
(
grants[]) instead of hunting for role differences between twoglobalOpsaccounts. - Bad: more upfront modeling work than a flat enum; requires
whisker-host(and any module doing its own authorization) to resolve capabilities frompersona+scope+grantsrather than checking a role string directly — a larger lift if authorization checks already exist as raw string comparisons.
Decision Outcome
Section titled “Decision Outcome”Chosen option: Option C — banded ladder with scope and grants as separate axes.
Data model
Section titled “Data model”type Scope = | { kind: 'global' } | { kind: 'storeList'; storeIds: string[] } // region, district, etc. | { kind: 'store'; storeId: string };
interface UserProfile { persona: Persona; // capability tier — fixed, small enum scope: Scope; // how far that capability reaches grants?: string[]; // named exceptions, e.g. "globalConfig:view" revokes?: string[]; // narrow removals from the default bundle}Authorization checks (pages, config sections, tooling) should resolve a
capability string (e.g. "storeConfig:write", "incidentTools:access")
from persona’s default bundle, filtered by scope, adjusted by
grants/revokes — never compare persona directly outside of that
resolution step.
persona bands
Section titled “persona bands”| Band | Types (highest → lowest) | Internal relationship | Default scope |
|---|---|---|---|
| Platform engineering | platformOwner → developer → support |
Each is the one above minus a gated slice, via revokes/default-off grants |
global |
| Global business admin | admin → globalOps → corporate |
Each is the one above minus a gated slice | global (nothing store-list-bound by default) |
| Store-local | storeOps → coach → athlete |
True nesting by scope breadth | storeList → store → store |
Bands are not ranked against each other — a support user is not
“below” a corporate user in some global ladder; they operate in
different domains. Only the ladder within a band is a strict order.
Per-type definition
Section titled “Per-type definition”Platform engineering band (technical tooling, scope always global):
platformOwner— new type. Infra, secrets, deploy/release pipeline, manifest-publisher controls, kill switches. Small number of people/service identities. Superset ofdeveloper.developer— full application/feature access: build, debug, feature flags, non-prod environments. No infra/secrets/deploy access — that’splatformOwner-only.support— incident-response tooling: logs, diagnostics, feature-flag toggles, remediation actions needed during an incident. Explicitly not given the developer-only configs meant to stay hidden from non-engineering-owned surfaces (e.g. internal debug panels tied to active development, not incident response). That boundary is arevokeslist off thedeveloperbundle, not a separate rank versuscorporate. Incident-triggered elevation beyond the defaultsupportbundle should be a time-boxed, auditedgrantsentry — not a permanent account change.
Global business admin band (business config, scope typically global):
admin— full business/product configuration authority.globalOps— identical toadminminus a specific set of gated global configs; unlocked per-config via an admin-issuedgrantsentry (e.g."globalConfig:viewBillingRules"), not by changing type.corporate— cross-store visibility and upload/content workflows; cannot mutate store-facing configuration. Least-privileged in this band.
Store-local band (scope narrows with each step down):
storeOps— operational permissions across an assigned subset of stores (scope.kind === "storeList").coach— elevated permissions, but scoped to their one store (scope.kind === "store").athlete— baseline permissions to log in and view what’s needed for their store (scope.kind === "store", mostly read).
Total: 9 persona values, in 3 bands of 3.
Positive Consequences
Section titled “Positive Consequences”developer’s blast radius shrinks — infra/secrets/deploy moves to a smallplatformOwnerset, matching least-privilege expectations.supportno longer has to be squeezed into a rank versuscorporate; it’s modeled as a restricteddevelopersibling, matching how the team actually thinks about it (tooling access, minus dev-only configs).- Store-subset variants (region, district, single store) never require a
new
persona— they’re ascopevalue. - Admin-gated config approval (
globalOps) and incident-driven tooling elevation (support) both have one consistent, auditable mechanism (grants[]) instead of ad hoc type variants. - Authorization logic in
whisker-host/modules can be tested against capability strings, decoupling page/config gating from the exactpersonalist — future type additions don’t require touching every call site.
Negative Consequences
Section titled “Negative Consequences”- Larger upfront implementation than a flat enum: needs a capability
resolver (
persona+scope+grants/revokes→ capability set) rather than direct role checks. - Any existing prototype/code that already checks
personastrings directly needs to migrate to capability checks — not yet audited as part of this ADR. grants/revokesneed an owner and an audit trail (who granted what, when, why) or they become as opaque as ad hoc role variants would have been — this ADR does not yet specify that storage/audit mechanism.
Recommendations
Section titled “Recommendations”- Adopt the 9-type, 3-band list above as
persona; addplatformOwneras a new type distinct fromdeveloper. - Implement
scopeas a field independent ofpersona, withglobal/storeList/storevariants. - Model
globalOps’s gated configs andsupport’s incident-elevation tooling as named, auditablegrants[]entries — not new types or permanent account changes. - Design the
grants/revokesaudit trail (who approved, when, scope of grant, expiry for incident-driven grants) as a follow-up decision before this ships to production. - Audit any existing
whisker-hostauthorization code for directpersonastring comparisons and migrate to capability-string checks resolved frompersona+scope+grants.
