Skip to content
Talk to our solutions team

Realms

A realm names the schema entity that holds a set of identities and the provider names allowed to authenticate against it. Realms are configuration composed per tenant — there is no realm table, no /admin/realm route, and no discovery endpoint. A client must know its realm name out of band.

iam.svc ships one built-in realm:

RealmEntityActiveDefaultProviders
usersusersyesyespassword

Every layer you add composes over that.

realms: is a map keyed by realm name, not a list.

iam/realms.yaml
realms:
users:
default: true
active: true
providers:
- password
- magiclink
patient:
active: true
entity: patient # your entity, from iam/extend/patient.yaml
providers:
- password
KeyTypeDefaultMeaning
realms.<name>.entitystringusersSchema entity holding this realm’s identities. Empty resolves to the base users entity.
realms.<name>.activebooltrue when absentAn inactive realm refuses login with 400 unknown_realm.
realms.<name>.defaultboolfalseMarks the realm used when a login request names none.
realms.<name>.providerslist of string[]Provider names this realm allows. An empty or absent list means password-only.

The same file may also carry a realmconfig.enable.agents / .bots / .delegations block. Those toggles are not realm settings — they remove the corresponding entities from the tenant’s composed schema entirely.

The realm’s entity must exist in the tenant’s composed schema (service base ⊕ product ⊕ tenant extensions). To authenticate against it the entity needs:

  • at least one field carrying attributes: {useforauth: true}, and
  • a field named password, which the engine argon2id-hashes on create and update.

A login against realm: patient reads the patient entity, and the minted token carries realm: "patient" with sub set to the row id in that entity — not in users. Two realms over two entities are two disjoint identity populations that happen to share a tenant.

Realms compose across three layers, last-write-wins per named realm:

OrderLayerSource
1Service defaultsBuilt in — realm users over entity users, provider password
2Productiam/realms.yaml
3TenantA tenant_extensions row at path /realms.yaml

A realm cannot be deleted by a later layer, only redefined. Setting active: false is how you retire one. Because the service defaults are always layer 1, the realm named users is always present in the composed map — a product cannot remove it, only redefine or deactivate it.

The default realm is whichever realm was last seen carrying default: true. A layer that declares realms but marks none of them default leaves the previous layer’s choice standing.

POST /auth/login resolves the realm before it looks at any credential:

  1. The request’s realm field, if present. A value equal to superadmin — compared case-insensitively after trimming — short-circuits the entire realm machinery and authenticates against the control plane instead.
  2. Otherwise the realm marked default: true.
  3. Otherwise the literal users.

The resolved name is then looked up in the composed map. A name that is absent, or present but inactive, fails. An empty entity on the resolved realm becomes users.

Steps 2 and 3 are name fallbacks, not escape hatches — the name they produce is looked up and gated like any other. Redeclaring users as active: false without marking another realm default: true makes every login that omits realm fail with 400 unknown_realm.

POST /auth/login
{ "identity": "[email protected]",
"password": "",
"identity_type": "mobile",
"realm": "patient" }

Errors from realm resolution, in the standard {"code": …, "error": …} envelope:

CodeHTTPWhen
unknown_realm400The named realm is not in the composed map. Message ends unknown realm "<name>".
unknown_realm400The realm exists but is inactive. Message ends realm "<name>" is inactive.
unknown_realm401On POST /auth/mfa/verify only — the realm carried by the mfa_token no longer resolves. Message: realm no longer exists.
provider_not_allowed403The realm’s providers list does not contain password.
bad_identity_type400identity_type names a column the entity did not declare as a login identifier.

Realm survives the flow: refresh preserves the realm the session started in, and the MFA-pending token carries it so the second step reads the same entity. The full password flow is on Password login.

providers: is a name gate. A provider must be declared in iam/providers.yaml for a realm to reference it, but the declaration carries exactly three fields — name, type, template — and no credentials, endpoints or callback URLs.

Nothing reads type or template at request time either, so type: oauth on a declared provider wires no identity provider. The consequence for the realm author is narrow and worth stating plainly: providers decides one thing — whether password login against this realm returns 403 provider_not_allowed.

Which columns can identify a user is declared on the field, in the entity definition, not in realms.yaml:

fields:
- name: email
type: string
attributes:
useforauth: true # this column is a login identifier
useforidentity: true # …and the default one, tried first
- name: mobile
type: string
attributes:
useforauth: true
- name: employeeid
type: string
attributes:
useforauth: true
AttributeEffect
useforauth: trueThe column may be matched against the request’s identity value.
useforidentity: trueAmong the useforauth columns, this is the one tried first.

Accepted truthy spellings: boolean true, or the strings "true", "yes", "1". Anything else reads as false.

Because the declaration lives on the field, it composes through the same layering as the rest of the schema — a product or tenant that adds an employeeid field with useforauth: true has added a login identifier without touching the auth configuration. See Identity entities for what a layer may add and what the final and access-lock seals refuse.

  1. The useforidentity column, if one is declared.
  2. The remaining useforauth columns, in declared field position order.

With no identity_type in the request, each column is tried in that order until one matches. A failure at any column returns the same 401 invalid_credentials — which column missed is never disclosed.

Sending identity_type narrows the lookup to exactly one column. The value is lower-cased and trimmed, then matched against the declared set. A name that is not in that set returns 400 bad_identity_type, and the message enumerates what is declared:

{ "code": "bad_identity_type",
"error": "identity_type \"username\" is not a declared login identifier (declared: email, mobile)" }

The service never filters on a column the entity did not mark.

An entity that declares no useforauth column at all falls back to ["email"]. The same fallback applies when the composed schema cannot be resolved for the tenant. A minimal product realm entity therefore logs in by email whether or not it says so.

EntityPlaneuseforidentityOther useforauth
userstenantemailmobile
superadmincontrolemailmobile

Both columns carry a unique constraint; email is additionally required, mobile is not.

realm: "superadmin" is not a realm you can declare. It is intercepted before tenant resolution and authenticates against the control plane’s own superadmin entity, signed by that plane’s own keyring, with roles: ["superadmin"] derived structurally from the path and never from row data. It bypasses realm resolution, the composed behavior config and the provider gate entirely, and it carries no MFA and no lockout policy. Declaring a realm named superadmin in realms.yaml has no effect — the interception happens first. The operator plane is covered on Superadmin.

Every request and response shape for these routes is in the IAM API reference; the full error table is on the error page.