Hierarchy and Namespaces

SaaSLayer Permissions has a small, fixed platform hierarchy: organizations, workspaces and groups. Your product's own structure (sites, buildings, projects, folders, devices) lives in scopes and resource names that you choose. This page states what is fixed, what you can extend, and what is not supported today. Each rule names the code that enforces it in Where this is enforced.

The fixed platform hierarchy

ContainerPublic APIAllowed parentNotes
Organization/orgsnoneOrganizations never nest.
Workspace/workspaces (ids tnt_…)an organization (orgId is required)Workspaces never nest.
Group/groupsa workspace, an organization, another group, or none (a root group)Groups nest, with a depth limit.

These three container types are the whole platform hierarchy. You cannot add a fourth container type, put an organization inside another organization, or put a workspace inside a workspace; the service refuses those edges. Container types in the public API are ORG, WORKSPACE and GROUP.

Group nesting depth

A group can sit at most four levels below the top of its chain:

Chain starts atGroup levels allowedExample
Workspace3org → workspace → group → group → group
Organization4org → group → group → group → group
Root group5 (the root plus 4)group → group → group → group → group

A deeper group is refused with resulting depth 5 exceeds max depth 4 for GROUP. Cycles are also refused.

Moving things

  • A group can move to a new parent with PUT /groups/{id}/parent, inside the same organization. Moves across organizations are refused.
  • Workspaces and organizations cannot be moved. Not supported today.

Principals and membership

The principals that hold permissions are USER, AGENT and SERVICE_ACCOUNT. Organizations, workspaces, groups and roles are containers or policy attachment points, never acting principals. A group cannot be a member of another group; group nesting is structural only.

Membership follows the hierarchy:

  • Joining a workspace requires membership in its organization.
  • Joining a group requires membership in the nearest workspace above it, or its organization when there is no workspace. Joining a group does not make you a direct member of its parent groups.

Without that parent membership the request fails with PARENT_MEMBERSHIP_REQUIRED, unless the caller asks for the parent membership to be created as well.

Membership alone grants nothing. Access comes from roles and policies (see Core Concepts).

All principal types share one permission domain: identity:principal (create, read, update, delete, import) covers users, agents and service accounts alike. There is no per-type variant.

Where permissions apply

A role assignment carries one scope string, and a check is made at one scope. A check sees:

  1. global entries (scope null),
  2. when the scope is a typed scope <kind>:<orgId>:<id>, the entries of the organization <orgId>,
  3. the entries assigned at exactly that scope.

A deny from any of them wins.

Assigned atApplies to a check at
null (global)every scope
org_acmeorg_acme, and every typed scope <kind>:org_acme:<id>
site:org_acme:site_1site:org_acme:site_1 only
a workspace id, e.g. tnt_123tnt_123 only

A workspace id is not a typed scope, so organization-wide assignments do not flow into a check made at a workspace scope. Check at the organization scope too when a workspace resource should honor organization-wide roles.

Policies attached to containers

Policy attached toApplies at
A workspacethat workspace's scope
A group placed directly in a workspacethat workspace's scope
An organizationthe organization scope and its typed scopes when the deployment setting OrgScopedDocs is on; every scope when it is off (the default)
A group whose parent is an organization, another group, or nothingevery scope

Attach scoped policies to workspaces or to groups placed directly in a workspace, or use roles assigned at an explicit scope.

Modeling your own hierarchy

Your product's structure belongs in scopes and resources, not in new platform containers.

Typed scopes for areas inside an organization

Use a typed scope <kind>:<orgId>:<id> for any area inside one organization, such as site:org_acme:site_1 or building:org_acme:b-7. You choose kind. The second segment is always the owning organization, which is how the service ties a grant to the organization being administered without knowing what a site is. Segments are non-empty and contain no :, # or control characters. The SDK helpers are typedScope(kind, orgId, id), siteScope(orgId, siteId) and parseTypedScope(scope).

Your backend is responsible for proving that <id> exists inside that organization before granting at it.

Typed scopes are one level deep. building:org_acme:b-7 does not inherit grants from site:org_acme:site_1, even if building 7 is on site 1. If you need that, check each level your product cares about, or assign at the organization.

Resource names

A resource is a type plus an optional id, for example device or document/doc-123. The full form is an XRN:

xrn:<product>:<service>:<region>:<scope>:<resourceType>/<resourceId>
xrn:acme:core:*:org_acme:document/proj-1/doc-9

The id is opaque to the service. Matching supports * and a trailing prefix wildcard per segment, so the pattern xrn:acme:core:*:*:document/proj-1/* matches every document whose id starts with proj-1/. Prefix matching is string matching, not a parent/child relationship.

Product, service and action names

Permissions are named <service>:<resource>:<action> for SaaSLayer's own services (for example orgs:workspace:create) and <product>:<service>:<resource>:<action> for your product (for example acme:core:device:read).

Reserved names:

NameReserved as
saaslayerproduct
scopes, register, registryproduct names in catalog routes
orgs, roles, policies, identity, keys, onboarding, permissions, audit, catalog, ops, users, deployerservice names, under any product

Other rules:

  • A service belongs to exactly one product. Registering a service another product owns is refused with ServiceOwnedByOtherProduct.
  • A catalog manifest's service name matches `^[a-z0-9][a-z0-9-]{0,63}

SaaSLayer Permissions has a small, fixed platform hierarchy: organizations, workspaces and groups. Your product's own structure (sites, buildings, projects, folders, devices) lives in scopes and resource names that you choose. This page states what is fixed, what you can extend, and what is not supported today. Each rule names the code that enforces it in Where this is enforced.

The fixed platform hierarchy

ContainerPublic APIAllowed parentNotes
Organization/orgsnoneOrganizations never nest.
Workspace/workspaces (ids tnt_…)an organization (orgId is required)Workspaces never nest.
Group/groupsa workspace, an organization, another group, or none (a root group)Groups nest, with a depth limit.

These three container types are the whole platform hierarchy. You cannot add a fourth container type, put an organization inside another organization, or put a workspace inside a workspace; the service refuses those edges. Container types in the public API are ORG, WORKSPACE and GROUP.

Group nesting depth

A group can sit at most four levels below the top of its chain:

Chain starts atGroup levels allowedExample
Workspace3org → workspace → group → group → group
Organization4org → group → group → group → group
Root group5 (the root plus 4)group → group → group → group → group

A deeper group is refused with resulting depth 5 exceeds max depth 4 for GROUP. Cycles are also refused.

Moving things

  • A group can move to a new parent with PUT /groups/{id}/parent, inside the same organization. Moves across organizations are refused.
  • Workspaces and organizations cannot be moved. Not supported today.

Principals and membership

The principals that hold permissions are USER, AGENT and SERVICE_ACCOUNT. Organizations, workspaces, groups and roles are containers or policy attachment points, never acting principals. A group cannot be a member of another group; group nesting is structural only.

Membership follows the hierarchy:

  • Joining a workspace requires membership in its organization.
  • Joining a group requires membership in the nearest workspace above it, or its organization when there is no workspace. Joining a group does not make you a direct member of its parent groups.

Without that parent membership the request fails with PARENT_MEMBERSHIP_REQUIRED, unless the caller asks for the parent membership to be created as well.

Membership alone grants nothing. Access comes from roles and policies (see Core Concepts).

All principal types share one permission domain: identity:principal (create, read, update, delete, import) covers users, agents and service accounts alike. There is no per-type variant.

Where permissions apply

A role assignment carries one scope string, and a check is made at one scope. A check sees:

  1. global entries (scope null),
  2. when the scope is a typed scope <kind>:<orgId>:<id>, the entries of the organization <orgId>,
  3. the entries assigned at exactly that scope.

A deny from any of them wins.

Assigned atApplies to a check at
null (global)every scope
org_acmeorg_acme, and every typed scope <kind>:org_acme:<id>
site:org_acme:site_1site:org_acme:site_1 only
a workspace id, e.g. tnt_123tnt_123 only

A workspace id is not a typed scope, so organization-wide assignments do not flow into a check made at a workspace scope. Check at the organization scope too when a workspace resource should honor organization-wide roles.

Policies attached to containers

Policy attached toApplies at
A workspacethat workspace's scope
A group placed directly in a workspacethat workspace's scope
An organizationthe organization scope and its typed scopes when the deployment setting OrgScopedDocs is on; every scope when it is off (the default)
A group whose parent is an organization, another group, or nothingevery scope

Attach scoped policies to workspaces or to groups placed directly in a workspace, or use roles assigned at an explicit scope.

Modeling your own hierarchy

Your product's structure belongs in scopes and resources, not in new platform containers.

Typed scopes for areas inside an organization

Use a typed scope <kind>:<orgId>:<id> for any area inside one organization, such as site:org_acme:site_1 or building:org_acme:b-7. You choose kind. The second segment is always the owning organization, which is how the service ties a grant to the organization being administered without knowing what a site is. Segments are non-empty and contain no :, # or control characters. The SDK helpers are typedScope(kind, orgId, id), siteScope(orgId, siteId) and parseTypedScope(scope).

Your backend is responsible for proving that <id> exists inside that organization before granting at it.

Typed scopes are one level deep. building:org_acme:b-7 does not inherit grants from site:org_acme:site_1, even if building 7 is on site 1. If you need that, check each level your product cares about, or assign at the organization.

Resource names

A resource is a type plus an optional id, for example device or document/doc-123. The full form is an XRN:

xrn:<product>:<service>:<region>:<scope>:<resourceType>/<resourceId>
xrn:acme:core:*:org_acme:document/proj-1/doc-9

The id is opaque to the service. Matching supports * and a trailing prefix wildcard per segment, so the pattern xrn:acme:core:*:*:document/proj-1/* matches every document whose id starts with proj-1/. Prefix matching is string matching, not a parent/child relationship.

Product, service and action names

Permissions are named <service>:<resource>:<action> for SaaSLayer's own services (for example orgs:workspace:create) and <product>:<service>:<resource>:<action> for your product (for example acme:core:device:read).

Reserved names:

NameReserved as
saaslayerproduct
scopes, register, registryproduct names in catalog routes
orgs, roles, policies, identity, keys, onboarding, permissions, audit, catalog, ops, users, deployerservice names, under any product

Other rules:

  • A service belongs to exactly one product. Registering a service another product owns is refused with ServiceOwnedByOtherProduct.
  • A catalog manifest's service name matches , and its role ids are <product>:<name>.
  • Catalog roles can be made grantable at the account (organization) scope and the site typed scope only. Other typed kinds work with roles you assign directly, but not as catalog grantableScopes.

Not supported today

RequestStatus
Extra platform levels (nested organizations or workspaces, a new container type)Not supported. Model the level as a typed scope or a group.
Groups deeper than the limits aboveNot supported.
Moving a workspace or an organizationNot supported.
Groups as members of other groupsNot supported. Nest groups structurally instead.
Inheritance between typed scopes (site → building)Not supported. Check each level.
Bring-your-own hierarchy (customer-defined container types and parent rules)Not supported. It would be built only against a concrete customer requirement.
Cedar principal is User in Group::"admins" (typed hierarchy scopes)Future work. The Cedar compiler parses it, but permission rebuilds do not apply the principal-type filter.
Catalog grantableScopes beyond account and siteNot supported.

Where this is enforced

Paths are relative to the SaaSLayer monorepo.

RuleCode
Container types, allowed parents, depth limitservices/permissions-service/src/hierarchy.js:13-32, :462-473
Public WORKSPACE name for the internal TENANT kindservices/permissions-service/api/workspace-contract.js:7-8
Workspace and group creation inputsservices/permissions-service/api/workspaces.js:43-45, services/permissions-service/api/groups.js:43-57
Group moves stay inside one organizationservices/permissions-service/src/hierarchy-move.js:85-96
Principal types, membership containersservices/permissions-service/src/principals.js:55-121
Required parent membershipservices/permissions-service/src/permissions-engine.js:8549-8600
Scope evaluation (global, typed scope's org, exact scope)packages/permissions-engine-core/src/permission-doc.js:285-296, packages/permissions-engine-core/src/scope-org.js:4-23
Where container policies apply; OrgScopedDocsservices/permissions-service/src/permissions-engine.js:1979-2000, services/permissions-service/saaslayer.module.js:31
XRN format and matchingpackages/permissions-engine-core/src/xrn.js:1-2, :90-142
Reserved product and service namesservices/permissions-service/src/builtin-services.js:4, :386-409, services/permissions-service/api/product-catalog.js:28
Service ownership by one productservices/permissions-service/src/permissions-engine.js:9732-9733
Catalog name rules and grantable scopesservices/permissions-service/src/product-catalog.js:10-12, :62
Typed hierarchy Cedar scopesservices/permissions-service/src/cedar.js:364-379, :1052-1057
Typed scope contractservices/permissions-service/docs/architecture/grant-scopes.md