Skip to main content
Amends 0240 (decision 1, the package that holds the tree) and 0323 (the package that owns ScopeRef). Adds Notes to 0127, 0202, 0208, 0210 and 0213, whose placement of the tree or of ScopeRef changes. Each amended page carries a note that says what changed and which of its decisions stand.

Context

The scope tree is the hierarchy that settings (0213), IAM (0208), feature flags and the other configuration consumers resolve against. 0240 placed the tree in @stackra/iam and @stackra/tenancy, so two packages held one structure, and a consumer that needed only a scope depended on tenancy. 0323 moved the tenant vocabulary out of the other packages. The owner decided in the final structure review of 2026-10-24 that the tree is one package of its own, and that tenancy is one of its consumers. Tenant isolation is a different matter, and it stays where it is.

Decision

  1. One package. @stackra/scope is a free package in the security category. It owns the hierarchical scope tree that settings, IAM, feature flags and the other configuration consumers resolve against. 0240 placed the tree in @stackra/iam and @stackra/tenancy. It moves to @stackra/scope, and neither package holds it. Depth stays a plan entitlement (0240, 0242).
  2. Levels are data. A deployment declares its levels in order. The defaults are platform, product and tenant. The tenant level is recursive, so child tenants sit under their parent in the tree of 0210. A deployment may declare further levels as data, as 0208 decision 1 allows for levels that a product declares. Adding a level is a change to that data. It needs no code and no migration.
  3. Nodes. A node has a scope_id, typed PublicId<"scp">, and a materialised scope_path on ltree with a GiST index, as 0240 decision 5 states.
  4. Aliases. A deployment may give a level a display name of its own. An organisation is a parent tenant (0243), so it is shown as an alias of the tenant level, for example “organisation”, and it is not a level of its own. Each alias is a lang key, so each locale has its own label.
  5. ScopeRef is a type. The contracts entry of @stackra/scope, imported as @stackra/scope/contracts, exports ScopeRef, which carries the public id and the path of a node. It is a type and not a port. The port of this package is the resolver of decision 7. 0323 names this package as the owner of ScopeRef.
  6. Current scope in the action context. The current scope is part of the action context (0136). A code block may run in a scope other than the current one, for emulation or impersonation. Each such switch is audited.
  7. Resolver chain. The scope of a request is resolved by a chain, in this order: a request header (X-Scope-Path, accepted only when its path is one the caller holds a grant for, and dropped otherwise), a token claim, tenancy, and a root fallback. Each step is a driver behind a resolver port (0142). The chain runs in the tenancy step of the action pipeline (0138).
  8. Tenancy is a consumer. @stackra/tenancy maps each tenant to a scope node, and it contributes the tenant level and its alias. Tenant isolation does not change. Postgres row-level security on tenant_id (0239) and BelongsToTenant stay in tenancy. Scope is for the configuration hierarchy, and it is not for row isolation.
  9. Not adopted.
    • No generic key-value table of values in @stackra/scope. Each consumer keeps its own typed table, keyed by scope_id. Settings already has one, and 0227 rejects a JSONB-for-everything store.
    • No automatic application-level WHERE filter on models with a bypass decorator. Isolation stays in the database, through row-level security.
  10. Entries. The package has the contracts, core, node, react and native entries of the v3 layout (0294 decision 15). It adds config and testing where it needs them, as that decision allows, and an entry is exported once its folder holds a file. The react entry provides a current-scope hook and a scope switcher.

Consequences

  • Settings, IAM and flags resolve against one tree, and a level added for a deployment needs no code change.
  • Tenancy keeps tenant isolation and gives up the tree, so tenant row isolation reads as it does today.
  • A consumer that stores values per scope writes its own typed table with scope_id. The package adds no filter to its rows.
  • A switch into another scope leaves an audit record, so impersonation is never silent.
  • The tenancy rule, the workspace-layout rule and the constitution’s tenancy and security pages are updated with this ADR, so no document still places the tree in @stackra/iam or @stackra/tenancy.

Enforcement

  • planned — tests in @stackra/scope for a level added as data with no code change, for the resolver order of decision 7, and for the depth check against the plan entitlement at tree creation (0240).
  • planned — the lint rule of 0323 decision 5 refuses a tenant identifier declared in @stackra/scope, so tenancy’s contribution stays in tenancy.
  • No check exists today. The package is not written yet.

Supersedes and sources

Supersedes: none. Amends: 0240 (decision 1) and 0323 (the owner of ScopeRef). Notes added to 0127, 0202, 0208, 0210 and 0213. Sources: the owner’s final structure review of 2026-10-24, restated in this ADR’s own words.