Skip to content
GitHub
Decisions

ADR-086: Tenancy hierarchy — `org` and `domain` naming

Context

The in-band decision-support shift introduced new top-level routing coordinates: (org_id, domain, action) per ADR-076 D2 / ADR-077 D2. World models are versioned per (org, domain); API keys are minted per (org, domain) per ADR-084 D1.

The prior tenancy model used account for the customer entity and workspace for the work-grouping under it. Under the shift:

  • account is renamed to org. The concept is unchanged — the customer entity that owns one or more lower-level groupings; auth and billing operate at this level.
  • workspace is replaced by domain. The concept shifts subtly: domain is the container within the org where the user groups all their related actions — world models are scoped per (org, domain), and actions live inside a domain’s world model. The hierarchical role is the same (one level below the customer entity); the new name reflects the action-grouping semantics.

This ADR aligns the tenancy hierarchy’s naming across the ADR corpus and pins the cardinality, scope-model resource names, role meanings, and tenancy-enforcement level. The substantive structure from ADR-006 and ADR-033 stands; only the resource names align.

Decision

D1 — org is the customer / account identity

org (column org_id) is the customer entity. One org per customer organization. Auth identity (per ADR-039), billing surface (per ADR-076 D5), and the account-level admin scope all operate at the org level. The prior term “account” is retired in favor of org for consistency with the (org, domain, action) routing coordinate.

D2 — domain replaces workspace as the action-grouping container

domain (column domain_id) is the container within an org where users group related actions. One org owns 1..N domains; each domain has exactly one parent org. Domains are the unit at which world models are scoped, world-model versions are activated, and decision modules deploy (per ADR-076 + ADR-085 D2).

The prior term workspace retires. The hierarchical role is unchanged from workspace in the prior tenancy model; the new name reflects that the grouping organizes actions for a particular operational scope (e.g., “ap_operations” domain holding payment-release actions, vendor-onboarding actions, etc.) rather than a general-purpose work-grouping.

D3 — API keys are minted per (org_id, domain_id)

Per ADR-084 D1: API keys carry (org_id, domain_id) as their scope. A key authenticates a caller as acting on behalf of a specific org within a specific domain; the key cannot reach actions in a different domain or another org’s domains. Rate limits and cost-observation segmentation use the same (org_id, domain_id) unit (per ADR-084 D1 + D2).

Keys may carry additional scopes (read/write capabilities per D4); the (org_id, domain_id) binding is the tenancy anchor independent of capability scope.

D4 — Scope model: resource names align; structural shape preserved

The scope model from ADR-006 D4 carries forward structurally; resource names align:

Prior scope nameAligned scope name
read:workspaceread:domain
write:workspacewrite:domain
approve:agentsapprove:agents (unaffected by the rename; deprecation status separate concern under the Spectral Agent retirement per ADR-078)
admin:workspaceadmin:domain
admin:accountadmin:org
read:agentsrenamed read:actions (action-registry read) per ADR-092
write:traceswrite:traces (retired with OTEL ingestion per ADR-074; no replacement)
read:operationsread:operations (unchanged; operator scope)
write:operationswrite:operations (unchanged; operator scope)

The action:resource shape (per ADR-006 D4) is preserved. The set of capabilities each scope grants is preserved. Only the resource names change.

One scope is net-new under the in-band shift and has no prior-model equivalent: decide:domain — the capability to call POST /api/decide (and the MCP equivalent per ADR-088) for a domain. The decision API itself is new per ADR-077, so the scope is too. It follows the action:resource shape with domain as the resource. API keys carry read:actions + decide:domain as their capability scopes (per ADR-092), bound to the key’s (org_id, domain_id) pair per ADR-084 D1.

D5 — Roles carry forward; meaning is per-domain (was per-workspace)

The role model from ADR-006 D5 carries forward structurally:

LayerRoleScopes (renamed)
Domain (was workspace)adminread:domain, write:domain, approve:agents (deprecation status separate), admin:domain, read:actions
Domaincontributorread:domain, write:domain, read:actions
Domainobserverread:domain
Org (was account)owneradmin:org + implicit admin-level domain access across all domains in the org
OrgoperationsAll scopes (Spectral staff)

The role names (admin / contributor / observer / owner / operations) are unchanged; the level where each role applies aligns (workspace → domain; account → org). The roles’ substantive permissions are unchanged.

Whether admin:domain automatically grants approve:modules (per ADR-080 D3) is resolved by ADR-092 D5: it does not — module approval is operator-only. The roles as named here are the v0 alignment baseline.

Operations is a Spectral-internal-org role, not customer-org membership. The operations role is held by Spectral staff who are members of a single Spectral-internal org — their home org (the act.actor_org_id of ADR-087 D2). Operators are never members of any customer org: they do not appear in a customer’s domain_members, and customer-facing membership/settings reads (scoped to the customer’s own org_id) never surface them. The role is persisted at the org level (the organization_role ∈ {owner, operations} store, the missing backing for the role enum) and travels in the JWT as app_metadata.organization_role — the claim path the Operations Start middleware and Codex Pages Function already gate on (ADR-039, ADR-046 D9); a top-level role is unavailable because the auth provider reserves it for the database role. Operators are identified and authorized by app_metadata.organization_role = operations, not by an enumerated operations entry in the scopes array (the operations capability “is everything implicitly” per the access-control model, so it is not scope-enumerated). The role’s cross-org reach is a capability exercised via assumed-identity per ADR-033 D4 + ADR-087 — the operator assumes one customer tenancy per request under RLS — not a standing membership and not a globally RLS-exempt connection.

D6 — Tenancy enforcement (ADR-033) operates at the domain level

ADR-033’s app-layer-primary + RLS-backstop posture carries forward structurally. The enforcement level aligns:

  • App-layer checks (per request, in business logic) use domain_id as the tenancy key (was workspace_id). The org_id is enforced one tier up — typically at API ingress, where the auth token’s (org_id, domain_id) binding determines what domain_id the request can target.
  • RLS policies at the database layer use the renamed domain_id column on tenant-scoped tables (was workspace_id). The core.users.role mirror per ADR-033 maps the caller’s role at the org/domain level.

Cross-org access at the data layer remains structurally forbidden by RLS (the per-domain_id policy implicitly excludes other orgs’ domains; auth-token validation at app-layer rejects mismatches before they reach RLS).

D7 — URL path conventions align

ADR-006 D2’s URL-path conventions (workspace-scoped routes carry the workspace identifier in the URL) carry forward structurally; resource names align:

  • /api/workspaces/{workspace_id}/.../api/orgs/{org_id}/domains/{domain_id}/... for domain-scoped routes.
  • Org-only routes (where the resource is at the org level, not domain) use /api/orgs/{org_id}/....
  • Non-tenant-scoped routes (auth, account profile, health, version) keep the prior /api/... shape; “account” routes (if any) align to “orgs”.

Per ADR-077 D6, the surviving routes (auth, accounts, workspaces, keys, operations) align: auth (unchanged), orgs (was accounts), domains (was workspaces), keys, operations.

The /api/decide endpoint is not URL-scoped per ADR-077 D1; routing coordinates ride in the request body. The org/domain alignment in this ADR confirms the body-shape coordinates established in ADR-077 D2.

D8 — Migration scope

The rename touches schema columns, RLS policies, app-layer code, URL routing, scope literal strings in auth middleware, role definitions, audit attribution. Migration is Phase 4 implementation work, not in scope here.

Key migration concerns the implementation epic must address:

  • Schema migration: rename workspaces table to domains; rename workspace_id columns across schemas; rename auth scope literals.
  • Backward-compatible alias: optional v0 transitional period accepting both workspace_id and domain_id in API responses, removed before GA. Out of scope for this ADR; the alignment is a hard rename.
  • Audit chain: existing audit records may carry the prior workspace_id column name; backfill or alias for historical queries.
  • API key reissue: if existing keys carry workspace_id claims, reissue under the new naming; transition window TBD.

Alternatives considered

Aliasing (treat org_id = account_id and domain_id = workspace_id at the API surface only; keep prior names internally). Rejected. Alias layers create confusion between API responses and underlying state; the rename is shallow enough to do once at v0 alpha rather than carry alias surface complexity forward.

Keep workspace and add org as a new parent layer (preserve workspace_id as-is; introduce org_id above it). Rejected per the user’s clarification: workspace is a dead concept, not a sibling to domain. Keeping it would introduce a third tier (account → workspace → domain) that doesn’t reflect the in-band shift’s two-tier (org, domain) reality.

Collapse domain into org (one-tier tenancy). Rejected per the user’s clarification. World models are scoped per (org, domain); collapsing the tier loses the action-grouping container that domains provide for organizing related actions under the same world-model boundary.

Rename workspacedomain but keep account as the customer-entity name. Rejected for consistency. The (org, domain, action) routing coordinate uses org; carrying account as a parallel name for the same concept creates two terms for one thing.

Defer the rename until after Phase 4 Linear scope cleanup (run Phase 4 with the old names, rename later). Rejected. The rename touches the same code Phase 4 will rewrite anyway (auth scopes, RLS policies, API routes); doing it in Phase 4 keeps the changes coherent and avoids a second migration pass.

Revise the role permissions during this ADR. Rejected as a separate concern. The roles carry forward unchanged per D5; permission review is implementation epic work, not naming-alignment work.

Consequences

  • ADR-006’s scope-model resource names align per D4 here; its structure stands.
  • ADR-033’s workspace → domain naming aligns per D6 here; its tenancy enforcement structure stands.
  • ADR-039 (Supabase Auth) operates at the renamed levels; specifics of the auth implementation (JWT claims, mirror table, session vars per ADR-041) align workspace-level → domain-level under Phase 4 implementation work. ADR-039’s substantive auth model is unchanged.
  • Schema migration is a Phase 4 epic: rename workspaces table → domains; rename all workspace_id columns; update RLS policies; update scope literal strings in auth middleware. Specifics belong in the migration epic, not in this ADR.
  • The Linear scope cleanup (Phase 4 cancellations + re-scopes) inherits the renamed vocabulary; cancellation notes and re-scope language use org / domain rather than account / workspace.
  • Codex content (apps/docs-codex) uses org / domain going forward; existing pages using account / workspace are touched up during the Phase 4 Codex rewrite.
  • API key minting per ADR-084 D1 lands cleanly on the renamed coordinates: (org_id, domain_id) is the key-scope tuple.
  • The role-permission review noted in D5 is a Phase 4 follow-up; this ADR commits the v0 alignment baseline without re-litigating permissions.
  • The org_id vs workspace_id alignment open item from ADR-077 Consequences is settled by this ADR.