0006. Identity boundaries: realm = issuer and user namespace; organization = business tenant#
Status: Accepted
Context#
The product must serve one person who belongs to several customer organizations without duplicating credentials, while also allowing genuinely separate identity namespaces (§4.3). An organization administrator must not be able to alter a shared user's global credentials (TEN-023).
Decision#
- Realm (
realmstable): the security boundary with a canonical issuerserver.public_origin + "/realms/" + name, its own users, identifiers, clients, API resources, roles, signing keys (signing_keys.realm_id) and security policy (realms.settings). Realms are created only through the operator CLI (identity-cli realm-create). A realm can be suspended (POST /api/v1/realms/{realm}/emergency/realm-state), which stops grant issuance and the custom APIs for that realm. - Organization (
organizationstable): a business tenant inside a realm. Users are realm-wide; their relationship to an organization is amembershipsrow (one live membership per user and organization, DB-004). Organization roles are assigned through the membership (role_assignments.membership_id, enforced by a CHECK constraint, AZ-003). - Organization policy (
organizations.policy) can only tighten realm policy (MFA required, shorter session lifetimes, allowed email domains, support-access opt-out and approval mode). - Global account operations (suspend, delete, credential and MFA reset) require
realm permissions (
realm.users.*); organization permissions (org.*) cannot reach them.
Alternatives considered#
- Organization = realm (one user namespace per customer): forces duplicate accounts for people in several organizations.
- Single global namespace without realms: no way to give a customer an independent issuer and user namespace.
Consequences#
- Separate realms share the deployment, database, root keys and operators. They are not physical isolation (§4.3, TEN-019). Stronger isolation needs separate deployments with separate keys and databases.
- Organization administration is reachable with an account-audience token
(
/api/v1/realms/{realm}/organizations/...); realm administration requires an admin-audience token from an MFA session. - The
sso_requiredorganization policy (withsso_provider_id) requires a federated factor from that organization's connection; no local-password alternative is offered to SSO-routed users, and switching into the organization without a federated factor fromsso_provider_id(any federated factor when it is unset) returnssso_required.TestFederationcovers this (TEST-027); the switch rules are listed in ADR 0012.
Update (2026-10)#
The decision stands. These additions to the organization boundary were made after acceptance:
Join requests (INV-013,
internal/organization/groups.go,TestMembershipLifecycle): a request creates apending_approvalmembership that grants nothing until a reviewer decides. Reviewers (users holdingorg.join_requests.reviewin the organization, at most 20 notified) are emailed that a request waits, without the requester's identity. An administrator withorg.members.writecan assign 1–10 reviewers to a request (POST /organizations/{org}/join-requests/{id}/reviewers); every assignee must holdorg.join_requests.reviewand none may be the requester, and then only the assignees or a reviewer who also holdsorg.members.writecan decide. Self-approval is refused. The requester sees only their own requests (GET /me/join-requests, never reviewers or member lists), can withdraw a pending one (POST .../join-requests/{id}/withdraw), and is emailed the decision. Requests expire after 14 days.Domain re-verification (TEN-009, migration
0030,TestDomainReverification): a verified domain keeps a digest of the DNS proof that verified it and is re-checked by the worker (domains.recheck) every realm policydomains.recheck_days; afterdomains.lapse_after_failuresconsecutive failed daily retries the verification lapses (stateexpired), so a domain whose proof was removed, for example after the domain changed hands, stops routing sign-ins and satisfying domain rules. See identifiers.md.Tenant fairness (TEN-018,
TestTenantFairness): organizations share the deployment, so one tenant's load is bounded: per-organization password and token budgets (security.rate_limits.login_per_organization,organization_per_window), a per-organization share of each worker batch of notifications (workers.tenant_batch_share) and a cap on an organization's concurrently running jobs (workers.max_jobs_per_organization). These bound starvation; they are not isolation (§4.3).Aliases and domain routing (TEN-011, migration
0034,TestOrganizationAliasesAndDomainRouting): a renamed organization keeps its former slug as an alias, administrators can reserve more aliases, and a name is never shared by two organizations of a realm, so a link ororganizationparameter that used an old name cannot start pointing at another tenant. Email-domain routing has one explicit rule instead of silent ambiguity: an exact verified domain wins, otherwise the longest verified parent domain that opted intoinclude_subdomains. See identifiers.md.Onboarding templates (TEN-017, migration
0032,internal/organization/templates.go,TestOrganizationTemplates): realm administrators keep versioned templates (organization policy, custom roles, groups and the roles granted to them); an organization administrator withorg.updateapplies one, after an optional dry run, in one transaction. An application only adds what is missing and replaces the organization policy; it never rewrites an existing role (a role with other permissions is a conflict and the application fails with409), and roles and grants are created with the applying administrator's authority, so templates cannot exceed delegation ceilings, and template roles cannot carryrealm.*orsupport.*permissions. A template can also define up to 20 service principals and 20 webhook subscriptions: service principals are created owned by the organization, allowed only for it, named<organization slug>-<name>and given roles in its scope, never credentials. An existing service principal counts as the template's only when its name is exactly<slug>-<name>for the organization's current slug or one of its former slugs (aliases); one with that name owned elsewhere is a conflict ("service principal ... exists outside this organization"). Subscriptions are organization-scoped, HTTPS and limited to public addresses, and their signing secrets are returned once inwebhook_secrets. Applying them needsorg.service_principals.writeororg.integrations.writein the organization, otherwise the plan reports a conflict. A grant that direct assignment would hold for a second administrator (a role that requires approval, or a privileged role for an agent service principal, AZ-018, MCH-017) is a conflict too ("... needs an independent approver: assign it through POST /role-assignments"); once it is approved directly, the template reports it unchanged. Templates do not cover clients, identity providers or credentials.Dynamic groups (TEN-014, migration
0033,internal/organization/dynamic.go,TestDynamicGroups): a group can take its members from a rule over realm profile attributes (allof up to 5equals/inconditions). Only attributes the realm schema marks as notuser_editablequalify, because a group can carry roles and users must not be able to grant themselves those roles by editing their profile; setting a rule needs the authority to grant the group's roles. Rules are recomputed when set and every 10 minutes by the worker (groups.dynamic), withrecomputed_at,member_countanderrorreported; a rule that stops validating fails closed and removes every rule-derived member. A rule that matches more than 10000 members records that as itserrorand leaves the memberships unchanged; they grant nothing while the error stands.member_countcounts actual members, not those skipped for incompatible roles. Manual membership changes are refused while a rule is set. Every member a rule adds or removes is audited (group.member.added/.removedwithsource: "dynamic", ordynamic_rule_setfor manual members dropped when a rule is set). Rule-derived memberships grant roles only while the group was recomputed withinsecurity.dynamic_group_max_staleness(1 hour by default) and its rule has no error (AZ-010), so a stopped worker does not freeze access. Removing the rule keeps the members listed, but their rule-derived memberships then grant nothing; remove and re-add a member to keep their roles.Group authority (TEN-008, TEN-013, AZ-018,
internal/organization/groups.go): adding a member to a group (POST /organizations/{org}/groups/{group}/members,org.groups.write; a removed or expired membership is refused with422onmembership_id, "the membership has ended") requires that the administrator could assign every role granted to the group and to its ancestor groups (403 forbiddenwithoutorg.roles.assign,403 grant_ceiling_exceededfor a role beyond their delegable set). Moving a group under another parent (PATCH /organizations/{org}/groups/{group}withparent_id;""moves it to the top level) requires the same for the roles of the new parent and its ancestors, and counts the height of the moved subtree againstlimits.max_group_depth(422onparent_id: depth limit, cycle or unknown parent). Both run the separation-of-duties check (ADR 0013).group.updatedcarriesparent_idandprevious_parent_idwhen the parent changed, and every group update increments the membership revision of every member of the group's subtree, which changes their decision cache keys and token authorization version.Domain conflicts (TEN-009): verifying a domain (
POST /organizations/{org}/domains/{id}/verify) that another organization of the realm already holds verified sets the claim's state toconflict, recordsorganization.domain.conflict(categorysecurity, outcomedenied, metadatadomain) and answers409 conflict("domain is verified by another organization"). Two verifications racing for the same domain also get409, without theconflictstate or audit event. The worker's re-verification only re-checks domains that are already verified. A removed or expired claim can no longer be verified (409, "the domain claim is <state>; add the domain again"), and a verification that races a removal fails (409, "the domain claim changed; add the domain again"). An expired claim must be removed (DELETE .../domains/{id}) before the domain can be added again.Organization connections (TEN-007, FED-006):
policy.sso_provider_idmust name a realm-wide connection or the organization's own; another organization's connection is refused when the policy changes (PATCH /organizations/{org},422onpolicy.sso_provider_id).POST /organizationsvalidates a policy given at creation the same way, and there only a realm-wide connection can be named (the organization has no connection of its own yet):422onpolicy.sso_provider_id("unknown connection, or one that belongs to another organization"). An organization's own connection never authenticates into another organization's context:- it is offered only in transactions for that organization or without an
organization context, and starting it in a transaction for another
organization is refused (
422onprovider); - a transaction whose organization context (requested or selected)
differs from the connection's organization is blocked: its state
carries
error: "connection_not_allowed"and no next action; - choosing another organization after signing in through it
(
select_organization) and switching to another organization (POST /me/organization-grants) get403("the connection used to sign in belongs to another organization").
Without an organization context the connection can still be used, and an unknown or deleted connection counts as another organization's.
- it is offered only in transactions for that organization or without an
organization context, and starting it in a transaction for another
organization is refused (
Provisioning connectors (PROV-001..PROV-009): an organization's members and groups can be provisioned through SCIM 2.0 by connectors bound to the organization; a connector acts only on what it provisioned, deprovisioning removes only the organization membership, and roles come only from approved mappings within a ceiling no wider than the creating administrator's (scim-provisioning.md).
Organization mail servers (TEN-022): an organization's owner can send its mail through the organization's own SMTP server; the password is encrypted under a key bound to the realm and organization and is never used for another tenant (ADR 0016).
Migration implications#
Moving users between realms is not supported and must never be done by email matching (DOM-006); it requires export, import into the target realm and re-verification.
Related requirement IDs#
DOM-002, DOM-003, DOM-005, DOM-006, DOM-007, DOM-008, TEN-003, TEN-004, TEN-007, TEN-008, TEN-009, TEN-011, TEN-012, TEN-013, TEN-014, TEN-015, TEN-017, TEN-018, TEN-019, TEN-022, TEN-023, INV-013, AZ-003, AZ-010, AZ-018, FED-006, PROV-001..PROV-009, §4.3