Tenants and tenant settings
A tenant is an isolated identity realm with its own issuer
(https://id.example.com/t/acme), users, clients, keys and settings. This page covers
creating and managing tenants and every key of the tenant settings document. For what
a tenant is, see Tenants and issuers.
The master tenant
Every deployment has a master tenant, created by the first migration. Its users are
global administrators (see Administrator access). It cannot be disabled or
deleted. Keep ordinary users and applications in their own tenants; master is for
the people who run the deployment.
Creating a tenant
Creating tenants needs ridm:tenants:create, which only a global owner holds.
| Where | How |
|---|---|
| Console | Tenants (/console/tenants/) → New tenant; you land on the new tenant’s settings |
| CLI | ridm tenant create acme --name "Acme Corp" |
| API | POST /admin/tenants |
POST /admin/tenants
Authorization: Bearer rpat_…
Content-Type: application/json
{ "slug": "acme", "display_name": "Acme Corp", "settings": { "registration": { "enabled": true } } }
slugis 1–63 lowercase letters, digits or hyphens, not starting or ending with a hyphen. It is part of every URL of the tenant (/t/acme/…) and cannot change.display_nameis 1–255 characters; it is shown on the login pages and in the console.settingsis optional: anything left out takes the defaults below.data_regionis optional: on a deployment with data regions, the region whose database keeps the tenant’s data (GET /admin/regionslists them; absent orhome: the home database). It is fixed at creation;ridm-api move-tenantmoves a tenant later.
A new tenant is seeded with the standard scopes (openid, profile, email,
phone, address, offline_access, features), the urn:ridm:admin and urn:ridm:account
resource servers, the six built-in admin roles, and the two built-in console clients
(ridm-admin-console, ridm-account-console). Its first signing key is made on first
use. It has no users: invite or create an administrator for it next (see
Users, invitations and bulk import).
Managing a tenant
| Task | Console | API | Permission |
|---|---|---|---|
| List tenants | Tenants | GET /admin/tenants?cursor=&limit= | ridm:tenants:read; tenant administrators see only their own |
| Read a tenant | Settings | GET /admin/tenants/{slug} | ridm:tenants:read |
| Rename, disable, change settings | Settings | PATCH /admin/tenants/{slug} | ridm:tenants:write |
| Delete | Settings → delete zone (global owners) | DELETE /admin/tenants/{slug} | ridm:tenants:delete, global |
| Export / import configuration | Export & import | GET …/export, POST …/import | ridm:tenants:export / ridm:tenants:import |
PATCH takes display_name, status (active or disabled) and settings. The
settings are a JSON merge patch (RFC 7396) over the stored document: send only what
changes, and null resets a key to its default. A field name the settings document
does not have is refused with 400 (“unknown settings fields: …”) rather than silently
dropped, because it is almost certainly a typo; that includes the removed
registration.captcha. Tagged settings switch variant cleanly: {"mfa": {"mode": "optional"}} replaces a required_for_roles policy without clearing roles by hand.
curl -s -X PATCH "https://id.example.com/admin/tenants/acme" \
-H "Authorization: Bearer $RIDM_TOKEN" -H "Content-Type: application/json" \
-d '{"settings": {"password": {"min_length": 14}, "mfa": {"mode": "required_for_admins"}}}'
A disabled tenant refuses every sign-in and token request, and its tokens stop working at the admin API; administrators can still read it through the admin API and re-enable it. Deleting a tenant permanently removes it and everything in it, with no soft-delete period.
The console’s Settings page (/console/settings/) shows every setting on one page and
saves as you go: changes are joined into one merge patch sent once typing pauses. The
header shows “Unsaved changes”, “Saving…”, “Saved”, or the API’s reason for refusing,
after which the stored settings are reloaded.
To keep settings in version control, export the tenant with
ridm tenant export acme -o acme.json and apply edits with ridm tenant import; see
Configuration as code.
The settings document
Every key has a default, and a stored document written by an older version keeps loading. The top-level keys:
| Key | Governs | Details |
|---|---|---|
password | Password rules and history | below |
session | Browser sessions, trusted devices, default token lifetimes | below |
mfa | When a second factor is demanded | MFA policy |
mfa_methods | Which second factors users may enrol | MFA policy |
risk | Scoring sign-ins, and what an unusual one costs | Adaptive authentication |
impersonation | Whether administrators may sign in as users, and for how long | Impersonation |
auth | Which first-factor sign-in methods are offered | below |
registration | Self-registration | below |
lockout | Brute-force lockout | below |
captcha | When a CAPTCHA is demanded | Rate limits, IP rules and CAPTCHA |
rate_limits | Request ceilings on the OAuth and sign-in endpoints | Rate limits, IP rules and CAPTCHA |
notifications | Security notices to users | below |
account | What users may do in the account console | below |
locale | Languages of the login pages | below |
branding | Look of the login pages | below |
keys | Signing key algorithm and rotation | below |
discovery | WebFinger issuer discovery | below |
dcr | Dynamic client registration: disabled, open or initial_access_token | Registering clients |
audit | Audit log retention | below |
custom_domain | The tenant’s own issuer host | Custom domains |
features | Feature flags applications read | Feature flags |
password
| Key | Default | Meaning |
|---|---|---|
min_length | 12 | Minimum characters |
max_length | 128 | Maximum characters |
require_uppercase | false | Needs an uppercase letter |
require_lowercase | false | Needs a lowercase letter |
require_digit | false | Needs a digit |
require_symbol | false | Needs a character that is neither alphanumeric nor whitespace |
history | 5 | A new password must differ from this many previous ones, the current one included (0 off) |
max_age_days | null | Days until a password expires and must be changed (null never) |
check_breached | false | Refuse passwords found in breach corpora |
The policy applies to users’ own changes, registration, invitations, admin-set
passwords (unless skip_policy is sent) and imported plaintext passwords. Imported
hashes are not checked, since the plaintext is unknown.
session
| Key | Default | Meaning |
|---|---|---|
idle_timeout_secs | 1800 (30 min) | A browser session ends after this long without use |
absolute_timeout_secs | 43200 (12 h) | A session ends this long after sign-in, whatever its use |
max_concurrent | 0 | Live sessions per user; the oldest is signed out first, with back-channel logout to its clients (0 unlimited) |
remember_device_days | 30 | How long a “remember this device” cookie skips the second factor |
access_token_ttl_secs | 300 | Default access token lifetime |
refresh_token_ttl_secs | 2592000 (30 days) | Default refresh token lifetime |
id_token_ttl_secs | 300 | Default ID token lifetime |
A client may set its own token lifetimes, which take precedence over these defaults (see Registering clients).
The session timeouts bound refresh tokens too. A refresh token issued without the
offline_access scope belongs to the browser session it came from: it stops working
once that session ends, whether by sign-out, idle_timeout_secs or
absolute_timeout_secs, and each refresh counts as activity that extends the idle
window. A refresh token issued with offline_access outlives the session’s timeouts
and lasts its own refresh_token_ttl_secs, though an explicit sign-out still revokes
it. Clients that need long-lived access without a browser, device-flow clients among
them, should request offline_access. See Tokens.
auth
First-factor methods shown on the login page.
| Key | Default | Meaning |
|---|---|---|
password | true | Username or email and password |
magic_link | false | A sign-in link by email |
email_otp | false | A one-time code by email |
sms_otp | false | A one-time code by text message (needs an SMS gateway, see Email, SMS and templates) |
passkey | false | Passwordless sign-in with a passkey; also enables passkeys as a second factor |
registration
| Key | Default | Meaning |
|---|---|---|
enabled | false | Show “create an account” and accept self-registration |
require_email_verification | true | New accounts stay pending until the emailed link is followed |
require_terms | false | Registrants must accept the terms |
terms_url | null | Terms of service link, shown to registrants and on the terms step |
privacy_url | null | Privacy policy link |
allowed_email_domains | [] | Only these email domains may register (empty: any) |
Whether registration demands a CAPTCHA is captcha.on_registration (see
Rate limits, IP rules and CAPTCHA); in the console it sits
under Settings → Passwords & lockout. The earlier registration.captcha key is gone:
a migration carried any true value over to captcha.on_registration, a PATCH naming
it is refused, and a tenant import ignores it.
The fields a registrant fills in beyond username, email and password come from the tenant’s profile schema (see Users).
lockout
| Key | Default | Meaning |
|---|---|---|
max_failures | 10 | Consecutive failed sign-ins before the account is locked temporarily (0 off) |
lock_minutes | 15 | How long that lock lasts |
ip_max_failures | 100 | Failures from one address within the window before it is throttled (0 off) |
ip_window_minutes | 15 | That window |
An administrator clears a lock with POST /admin/tenants/{slug}/users/{user}/unlock
or the Unlock button on the user page.
notifications
Security notices sent to the user by email, or by text message when they have no
email address. All default to true.
| Key | Sent when |
|---|---|
new_device | Someone signs in from a device not seen before |
password_changed | The password changes |
mfa_changed | A second factor is added or removed |
email_changed | The email address changes (sent to the previous address) |
account
What users may do to their own account in the account console. These keys are not on
the console’s Settings page; change them with PATCH /admin/tenants/{slug} or a tenant
import.
| Key | Default | Meaning |
|---|---|---|
self_deletion | true | Users may delete their own account |
deletion_retention_days | 30 | Days a deleted account (by the user or an administrator) is kept before the daily purge removes it |
personal_tokens | true | Users may mint personal access tokens |
personal_token_max_days | 365 | Longest, and default, personal access token lifetime (0 no limit) |
locale
| Key | Default | Meaning |
|---|---|---|
default | "en" | Language used when neither the request nor the user names a supported one |
supported | ["en"] | BCP 47 tags the login pages and messages may use |
Tags are normalised on save (pt_BR becomes pt-BR); a malformed tag, or a default
outside supported, is refused. An empty supported list becomes [default]. The
language of a page or message is negotiated from the OIDC ui_locales parameter, then
the user’s stored locale, then default; a tag matches exactly or by language
(de-CH matches a supported de). Right-to-left languages (Arabic, Hebrew, Persian,
Urdu and others) set dir="rtl". The flow state carries the result as locale, dir
(ltr or rtl) and the selectable locales; every email or SMS the flow sends is
rendered in that locale, and a self-registered user is stored with it.
The bundled UI ships English strings only (ui/src/i18n/en.json). An added bundle
falls back key by key (de-CH, then de, then English), so a partial translation
still renders; plurals follow Intl.PluralRules, and the stylesheets use logical
properties so right-to-left languages mirror the layout. Message templates can be overridden per locale; see
Email, SMS and templates.
branding
How the tenant looks: the sign-in pages, the account console, and the admin console while it administers this tenant. Settings → Appearance in the console edits it with a live preview.
| Key | Default | Meaning |
|---|---|---|
logo_url | null | Logo on the login pages |
favicon_url | null | Browser tab icon |
primary_color | null | The theme’s primary colour: buttons, navigation, links, focus. A hex colour (#rrggbb) drives a full set of light and dark shades; any other CSS colour literal sets the plain accent only |
contrast_color | null | The theme’s second colour, for what must read as different from the primary: selected rows, the tenant mark. Hex; derived from the primary when unset |
background_color | null | Page background behind the card (a CSS colour literal) |
skin | "signal-v2" | The visual skin: signal-v2 (default), signal, vault or registry |
mode | "system" | Light/dark policy: system follows each person’s device, light and dark force one |
allow_theme_switch | true | Show a light/dark switch so people can override mode on their device |
support_url | null | “Need help?” link |
headline | null | One line for the sign-in page’s brand panel (at most 120 characters). With the Signal v2 skin the page splits into the panel and the form once this is set; the other skins keep the single card |
tagline | null | A sentence under the headline (at most 240 characters) |
custom_css | null | Stylesheet applied to the login pages after the theme |
links | [] | Extra footer links, each {"label": "…", "url": "…"} |
Colours are checked on write: a value that is not a CSS colour literal is refused, and
contrast_color must be hex. The console shows the contrast ratios the two colours
achieve on the page and on buttons in both modes, and flags anything under 4.5:1, but
it does not refuse a brand colour over it.
Branding, the display name, locales and the offered sign-in methods are published at
GET /t/{slug}/branding, a public, cacheable document every end-user page loads. The
console’s appearance editor (Settings → Appearance) frames the real login page in
preview mode and applies each draft change as you type. Images are loaded from their
URLs over https; the pages’ Content-Security-Policy allows images and fonts from any
https origin, so host them anywhere.
keys
| Key | Default | Meaning |
|---|---|---|
default_alg | "RS256" | Algorithm for keys made by rotation: RS256, RS384, RS512, ES256, EdDSA |
rsa_bits | "B2048" | RSA key size: B2048, B3072, B4096 |
rotation_interval_days | 90 | Rotate the active key automatically after this many days (0 only on demand) |
retire_overlap_hours | 24 | How long a retired key stays published so tokens it signed still verify |
See Rotating keys.
discovery
| Key | Default | Meaning |
|---|---|---|
email_domains | [] | WebFinger acct:user@<domain> lookups for these domains resolve to this tenant’s issuer |
audit
| Key | Default | Meaning |
|---|---|---|
retention_days | 365 | Days to keep audit rows (0 forever) |
See Webhooks and the audit log.
features
A map of flag name to flag, default {}: each with enabled, an optional
description and optional per-organization values. rIDM itself doesn’t act on them;
applications read them through the features scope or GET /t/{slug}/features. Edit
them on the console’s Feature flags page. See Feature flags.
{ "features": { "beta_dashboard": { "enabled": true, "organizations": { "globex": false } } } }
custom_domain
A hostname such as login.acme.example (optionally with a port), lower-cased on save.
It must not be the deployment’s own host and must not be used by another tenant.
Default null, meaning the issuer is {PUBLIC_URL}/t/{slug}. See
Custom domains.
Settings kept outside the document
Some tenant configuration holds secrets and so lives behind its own endpoints, stored encrypted and never returned:
| Configuration | Endpoints | Page |
|---|---|---|
| CAPTCHA provider (site key, secret) | GET/PUT/DELETE /admin/tenants/{slug}/captcha | Rate limits, IP rules and CAPTCHA |
| Email and SMS providers, templates | /admin/tenants/{slug}/messaging/… | Email, SMS and templates |
| User profile schema | GET/PUT /admin/tenants/{slug}/profile-schema | Users |
| IP allow and deny rules | /admin/tenants/{slug}/ip-rules | Rate limits, IP rules and CAPTCHA |
Limits
Some configuration is read whole (by the console, the tenant document and token
issuance), so each tenant has a ceiling on it. Creating one more past the ceiling is a
400:
| Collection | Per tenant |
|---|---|
| Groups | 10,000 |
| Roles | 5,000 |
| Scopes | 1,000 |
| Resource servers | 1,000 |
| IP rules | 1,000 |
| Webhooks | 100 |
| SCIM tokens (live) | 100 |
| Trusted certificate authorities (mutual TLS) | 50 |
| Feature flags | 200 |
Users, clients, organizations and the audit log have no ceiling: their lists are paginated.