Agora

Admin Console

The opt-in IdP admin console — a bundled React SPA with dashboard, users, sessions, orgs, clients, signing keys, audit, and runtime settings.

AuthKit ships an opt-in admin console: a metrics dashboard, user and role management, session context and revocation, organizations, the OAuth client list, signing-key rotation, the audit log, an impersonation helper, and an editor for the runtime settings. It is off by default, so adding or upgrading the package never exposes it by accident — an existing host sees no change in behaviour until it says so in config/authkit.ts.

Enabling it

Turning the console on is a single decision, taken in config/authkit.ts:

config/authkit.ts
defineConfig({
  // ...
  admin: {
    enabled: true,
    roles: ['ADMIN'], // global roles allowed into /admin; default ['ADMIN']
  },
})
start/routes.ts
registerAuthHost(router, {
  mountPath: '/oidc',
})

registerAuthHost inherits the on/off switch from the config — you do not repeat it in start/routes.ts. Whether the console exists is policy, and policy is owned by the config file so that config/authkit.ts can be audited on its own, without also reading every app's start/routes.ts. Declaring admin in defineConfig therefore locks the admin route option: passing admin: true alongside a config that already says enabled: true is harmless and redundant, while passing an option that contradicts the config is ignored, reported on the returned route map (overriddenByConfig), and warned about at boot. See Config locks for the general rule and the full list of locked keys.

FieldTypeDefaultNotes
enabledbooleanfalseMounts the admin console group behind the admin guard.
rolesstring[]['ADMIN']Global roles that grant access to the console. An empty array falls back to ['ADMIN'].
impersonationbooleantrueKill switch for impersonation — the RFC 8693 grant and the console panel. See below.

The impersonation kill switch

admin.impersonation is the one field here that reaches outside the console. It governs two surfaces at once:

  1. the RFC 8693 urn:ietf:params:oauth:grant-type:token-exchange grant registered on the OIDC provider — the mechanism by which an admin trades their own access token for one that acts as another account;
  2. the console's impersonation panel (GET {prefix}/api/impersonation/:userId).

Setting it to false removes both. The grant is never registered, so the token endpoint answers unsupported_grant_type, and the panel endpoint answers 404. It is a real kill switch, not a hidden button:

config/authkit.ts
defineConfig({
  // ...
  admin: {
    enabled: true,
    roles: ['ADMIN'],
    impersonation: false, // no token-exchange grant, no console panel
  },
})

The default is true, not false. The token-exchange grant has always been registered, and hosts consume it from their own admin tooling without mounting this library's console at all — defaulting to false would break them silently at runtime. If your deployment has no use for impersonation, turn it off explicitly.

Declaring admin.impersonation also locks the matching admin_impersonation runtime setting: the config file becomes the authority, reads of the setting resolve to the config value, and a write through the console's settings endpoint or the Admin API is rejected with 423 Locked (setting_locked, lockedBy: "config"). GET {prefix}/api/settings returns the locked keys in a locked array so the UI can render the control disabled with a "defined via defineConfig" note. Leave the key out of defineConfig if you want operators to flip the panel at runtime instead. See Config locks and Impersonation for the full mechanism.

Configurable admin prefix

By default the console is mounted under /admin. You move it by passing an object with a prefix to registerAuthHost:

start/routes.ts
// Default — console under /admin
registerAuthHost(router, {
  mountPath: '/oidc',
})

// Custom prefix — console under /auth/admin
registerAuthHost(router, {
  mountPath: '/oidc',
  admin: { prefix: '/auth/admin' },
})

The admin option has two independent axes, and they resolve in opposite directions. Whether the console exists is policy — the config wins, as described above. Where it lives is structural, and the call site wins: mounting is a routing decision, and only a function call can express something a config object cannot (mounting twice under two prefixes, for instance). So { prefix } is honoured even when admin is declared in defineConfig; only an on/off intent that contradicts the config is ignored.

The prefix is normalised automatically: it always starts with / and never has a trailing slash. All console routes, API calls, and assets follow the effective prefix — no further configuration is required. The Admin REST API (/api/authkit/v1) is not affected by this option.

Acme example — Acme mounts the console at /auth/admin to avoid collisions with an existing /admin route in their main app:

start/routes.ts
registerAuthHost(router, {
  mountPath: '/oidc',
  admin: { prefix: '/auth/admin' },
  adminApi: true,
})

The dashboard is then at https://auth.acme.com/auth/admin. The SPA keeps all navigation and filter state in the URL query string via nuqs (?view=users, ?view=users&q=ana&page=2, …), so every page and filter is deep-linkable and the same shell at {prefix} serves them all.

When prefix is omitted inside the object form (admin: {}), the default /admin is used — identical to admin: true.

What you actually get

The console is a self-contained React single-page application that ships inside the published @adonis-agora/authkit-server package, already built. Installing the package and setting admin.enabled is the whole story: there is no Vite config to add, no dev server to run, no build script to wire into your deploy, and no static files to copy into your app's public/ directory. The routes serve the application and its assets straight out of the installed dependency.

What that buys you, and what you can rely on:

  • Deep-linkable URL state — page navigation and per-page filters live in the query string via nuqs: ?view=users, ?view=users&q=ana&page=2, and so on. Every view and filter combination survives a refresh and can be pasted to a colleague. Switching pages clears the shared filter params so one page's search never leaks into another. Ephemeral UI — modals, half-filled forms — deliberately stays out of the URL.
  • Everything goes through the JSON API — the shell is static HTML; all data is fetched client-side from {prefix}/api/*, behind the same guard as the shell itself. Those endpoints are consumed with @adonis-agora/authkit-react's typed client and its TanStack Query hooks (useUsersQueryOptions, useCreateUserMutationOptions, …). If you are building your own admin interface, you are not reverse-engineering a private protocol — the console is the dogfood, and the hooks are public. See Typed Client & TanStack Query for the catalog.
  • No hard-coded paths — the shell injects a small runtime config object into the page (the effective admin base, the CSRF token, the signed-in admin, and the API base URL), and the application reads its endpoints from there. This is what makes the configurable prefix work end-to-end: move the console and the assets, API calls, and links all move with it.
  • Cacheable assets — hashed JS/CSS are served under {prefix}/assets/* with Cache-Control: public, max-age=31536000, immutable, so a browser fetches them once per release.
  • Dark/light theme — follows the operating-system preference (prefers-color-scheme), with a violet accent (#625fff).

Working inside a clone of the AuthKit repository rather than on an installed package? The shipped bundle only exists after the package has been built. Until then the console routes render a placeholder page telling you to build it — the routes, the guard, and the JSON API are all live regardless, so backend work does not need the front end built.

The guard

The adminGuard (exported, if you want to reuse it on routes of your own) protects the whole group — the shell and every {prefix}/api/* endpoint. In order, it requires:

  1. admin.enabled: true — when the console is disabled, the guard responds with 404 Not Found (the routes simply don't exist as far as a caller can tell);
  2. an active account session (otherwise a redirect to the account login URL, default /account/login, carrying a return_to);
  3. a session that has not idled out — the account console's idle timeout applies here too, and an expired one redirects to login with reason=idle;
  4. the signed-in account having at least one of admin.roles (otherwise a redirect to the configured accountHome — default /account/security — so the existence of /admin is not leaked).

Two details worth knowing. The admin.enabled check at step 1 is a genuine safety net, not a formality: routes are registered before the lazy config resolves, so the guard is the thing that makes a disabled console actually behave as if it were never mounted. And the roles at step 4 are resolved through the host's role authority — the same resolveTokenRoles hook the token claims are minted from, when one is configured — falling back to the account's stored global roles otherwise. An admin whose role lives in your application's own tables therefore reaches the console without being duplicated into AuthKit.

See Security for how the disabled-console 404 fits the broader hardening.

Routes

The SPA shell is served for every page route; actual data is fetched from JSON API endpoints under {prefix}/api/*. All endpoints are protected by the adminGuard.

RouteNotes
GET {prefix}Shell HTML — SPA loads, nuqs drives URL state
GET {prefix}/*Shell HTML — catches all SPA deep links
GET {prefix}/assets/*Bundled JS/CSS/fonts (hashed names, Cache-Control: immutable)
GET {prefix}/api/overviewDashboard metrics (total users, MAU, sessions, daily charts)
GET {prefix}/api/usersList/search users — ?search&page&size, paginated { meta, data }
GET {prefix}/api/users/:idUser detail
POST {prefix}/api/usersCreate user
PATCH {prefix}/api/users/:id/rolesUpdate roles
POST {prefix}/api/users/:id/disableDisable account
POST {prefix}/api/users/:id/enableEnable account
POST {prefix}/api/users/:id/reset-passwordSend password reset email
DELETE {prefix}/api/users/:idDelete user (cascade + anonymize)
GET {prefix}/api/users/:id/sessionsList a single user's active sessions
POST {prefix}/api/users/:id/revoke-sessionsDisconnect a user from all devices (revoke all their sessions + grants)
GET {prefix}/api/sessionsList active sessions/grants (all accounts)
POST {prefix}/api/sessions/revoke-allRevoke all sessions for an account
GET {prefix}/api/clientsList adapter-stored OIDC clients
POST {prefix}/api/clientsCreate a dynamic client
PATCH {prefix}/api/clients/:idEdit a dynamic client
DELETE {prefix}/api/clients/:idDelete a dynamic client
POST {prefix}/api/clients/:id/regenerate-secretRegenerate client secret
GET {prefix}/api/rolesList the roles catalog
POST {prefix}/api/rolesAdd a role to the catalog
PATCH {prefix}/api/roles/:nameUpdate a catalog role
DELETE {prefix}/api/roles/:nameRemove a catalog role
GET {prefix}/api/orgsList organizations
POST {prefix}/api/orgsCreate an organization
GET {prefix}/api/orgs/:idOrg detail
PATCH {prefix}/api/orgs/:idUpdate an organization (name, slug, logo)
DELETE {prefix}/api/orgs/:idDelete an organization
POST {prefix}/api/orgs/:id/membersAdd a member
PATCH {prefix}/api/orgs/:id/members/:accountIdChange a member's role
DELETE {prefix}/api/orgs/:id/members/:accountIdRemove a member
POST {prefix}/api/orgs/:id/invitationsCreate an email invitation
DELETE {prefix}/api/orgs/:id/invitations/:invitationIdRevoke a pending invitation
GET {prefix}/api/auditBrowse audit log — ?type&subject&page&size, paginated { meta, data }
GET {prefix}/api/settingsRead all runtime settings (add ?organizationId= for org-scoped)
PUT {prefix}/api/settings/:keyUpsert a setting — 423 when locked by config
DELETE {prefix}/api/settings/:keyDelete a setting (reset to lib default) — 423 when locked
GET {prefix}/api/keysManaged signing-key status
POST {prefix}/api/keys/rotateRotate the managed signing key
GET {prefix}/api/impersonation/:userIdGet impersonation panel data

Registration order matters here and is intentional: the assets route and every {prefix}/api/* endpoint are registered before the {prefix}/* catch-all that serves the shell. AdonisJS matches wildcards in registration order, so the reverse would have the catch-all swallow API calls and answer them with HTML — which surfaces in the browser as a JSON parse error rather than as a routing bug.

Dashboard

The landing view ({prefix}, i.e. ?view=overview) is a metrics overview. It is a single call to GET {prefix}/api/overview, which combines the account store, the OIDC adapter, and the audit sink into one payload:

  • Total users — count from the account store.
  • Active sessions — count of current sessions (when the adapter supports enumeration).
  • MAU (Monthly Active Users) — unique accounts with a login.success in the last 30 days.
  • Sign-ins / sign-ups — totals plus a per-day series for the same window, charted in the browser.
  • Clients — how many OAuth clients the resolved config declares. Note that this counts the config's client list, not the adapter's: clients created at runtime through the Clients page do not move this number. The Clients page itself reads the adapter and is the accurate picture.
  • Recent activity — the five most recent audit events.

Everything after the user count depends on optional capabilities, and the response says so rather than guessing: when the audit sink cannot be queried (list is not implemented) the payload reports auditSupported: false, MAU and the per-day series come back zeroed, the audit total is 0, and the recent-activity list is empty. The static counts still render. Active sessions behave the same way: the adapter's enumeration is optional, and the count is null rather than a fabricated zero when it is unavailable.

Session context

Every session displayed in the console (user detail page) carries rich context captured at login time:

FieldSource
BrowserParsed from the User-Agent string (e.g. Chrome 124)
OSParsed from the User-Agent string (e.g. macOS)
IP addressRequest IP at login
LocationResolved via the host's resolveGeo hook (optional)

To add geolocation, plug in a resolveGeo hook in defineConfig:

config/authkit.ts
resolveGeo: async (ip) => {
  // example: return 'São Paulo, BR'
  const geo = await myGeoLookup(ip)
  return geo ? `${geo.city}, ${geo.countryCode}` : null
},

The lookup is fail-safe with a 1.5 s timeout: an error or timeout returns null (no location shown) and never delays the listing.

Sessions & revocation

The user detail drawer lists the account's active sessions (IdP logins) and grants (per-client authorizations), with a live count of access / refresh tokens per grant (GET {prefix}/api/users/:id/sessions). The drawer's Actions row exposes a "Disconnect all devices" button that revokes every session and grant for that single user (POST {prefix}/api/users/:id/revoke-sessions) — the admin-side equivalent of the self-service "Sign out of all devices" action on /account/security.

The global Sessions page lists sessions and grants across every account and offers the same revocation from there, via POST {prefix}/api/sessions/revoke-all. Despite the route name, that endpoint is also per account: it requires an accountId (query, body, or route param) and revokes everything belonging to that one account. There is no revoke-the-entire-IdP button — that would sign out every user of every relying party in one click, which is not an action a console should make one keystroke away.

Both paths go through the exported AdminSessionsService, so a host that wants the same behaviour from a job or a command can call it directly.

Revocation works by destroying the account's Session and Grant artifacts directly through the OIDC adapter. Destroying a grant cascades to its tokens: oidc-provider validates every access/refresh token against Grant.find(token.grantId) and rejects it with invalid_token when the grant is gone. As a belt-and-braces measure the service also destroys the access/refresh token rows that reference the revoked grants (when the adapter can enumerate them). The operation emits a session.revoked_all audit event with the counts in metadata.

Like the client list, enumeration relies on the adapter's optional list?() capability (the database and Redis adapters both implement it). When the adapter can't enumerate, the page degrades gracefully with a notice instead of erroring. Both the database adapter's table query and the Redis adapter's SCAN are scoped per model, so the lookup is cheap.

Impersonation panel

A user's detail drawer can hand you the exact RFC 8693 token-exchange parameters needed to assume that user's identity, served by GET {prefix}/api/impersonation/:userId. The response carries the token endpoint, the grant_type, the subject_token_type, the requested_subject, the client id used in the example, and a ready-to-run curl snippet. It does not perform the exchange, and there is no link to follow: the exchange has to be signed with your own admin access token as the subject_token, and the console authenticates with a session cookie rather than an OAuth token. So the drawer shows the parameters and a copyable request, which you run from wherever that token already lives.

The panel offers whichever of your registered clients carries the token-exchange grant. A confidential client's secret is not recoverable — it is shown once at creation — so the snippet leaves a <CLIENT_SECRET> placeholder for you to fill in.

Availability is decided in two layers, deliberately asymmetric:

config.admin.impersonation decides whether the capability exists at all. This is a boot-time decision: it is what registers (or does not register) the RFC 8693 grant on the OIDC provider. It defaults to true. When it is false the panel endpoint answers 404 — returning parameters for a grant the provider does not implement would be a lie. See the kill switch above.

The admin_impersonation runtime setting decides whether the console offers the panel. This one is changeable without a redeploy. It only ever tightens: the config gate is evaluated first, and when admin.impersonation is declared in defineConfig the setting key is locked and the config value wins outright. When the setting is absent — or the auth_settings table does not exist — the resolver falls back to the config value, so the panel stays available by default rather than failing closed on a missing table.

# Turn the panel off at runtime, without a redeploy
# (only works when admin.impersonation is NOT declared in defineConfig)
node ace authkit:settings:set admin_impersonation '{"enabled":false}'

A third condition is structural rather than configurable: the panel needs a client carrying the urn:ietf:params:oauth:grant-type:token-exchange grant to build its example against. It looks for one among the clients your OIDC adapter holds — the ones created through the Client CRUD page, the Admin API or authkit:clients:create — and falls back to any static clients still declared on the config. When no client anywhere carries the grant, the endpoint answers 404 with no_token_exchange_client: there is no exchange to describe. Give an existing client the grant, or create one, and the panel appears.

An adapter that cannot enumerate its clients leaves only the config fallback. Every adapter this library ships can enumerate; a custom adapter without a list method cannot, and the panel will report no_token_exchange_client on such a host even though the exchange itself works fine — the grant runs against any adapter-stored client that carries it, enumerable or not.

The exchange itself requires a valid admin access token as the subject_token; the token endpoint validates the caller's admin role independently of anything the console does. The panel only shows the parameters, so opening it records impersonation.panel_viewed with channel: "admin-console" — not impersonation, which belongs to the exchange and is written by the token endpoint when an identity is actually assumed.

See Impersonation for the grant itself, the act claim, and how the impersonated session behaves.

User management (create / invite / reset / disable / delete)

The users page has a create form (email + optional name + optional password). When you leave the password blank the account is created with a throwaway password and a password-reset email is sent — the user sets their own password (the "invite" flow). Each row also exposes Send password reset and, when the store supports it, Disable / Enable buttons. The operations emit user.created, user.password_reset_sent, user.disabled, user.enabled, and user.deleted audit events. The Delete action runs the same cascade as the self-service flow: sessions, grants, PATs, passkeys, provider identities, and org memberships are removed; audit records are anonymized. See Compliance for the full cascade details.

Disabling requires the optional AccountStatusCapability on the account store (see Account store). The Lucid store implements it via a disabled_at timestamp column only when the column exists on your model — otherwise the capability is genuinely absent and the disable/enable buttons are hidden. Add the column with a migration of your own:

table.timestamp('disabled_at').nullable()

Both login paths (the OIDC interaction and the account-console login) reject disabled accounts with an "account disabled" message, even with a correct password.

Client CRUD

Clients are meant to be managed at runtime rather than declared in a config file. Create them from this console, the Admin REST API, or the authkit:clients:create command (see Reference) — no redeploy needed. defineConfig does accept a clients array, but it is an internal seam for tests and one-off migrations, not the production path.

The clients page lists all adapter-stored clients created via the console or Dynamic Registration. Adapter-stored clients are fully manageable via AdminClientsService:

  • Create — generates a client_id when omitted; for confidential clients (tokenEndpointAuthMethod !== 'none') it generates a client_secret and shows it once (it isn't recoverable afterwards). Public clients get no secret.
  • Edit — updates the editable metadata (redirect URIs, post-logout redirect URIs, grants, token-endpoint auth method, backchannel logout URI, session_required) while preserving the existing secret.
  • Regenerate secret — issues a fresh client_secret for a confidential client (shown once); rejected for public clients.
  • Delete — removes the client from the adapter.

Every write builds the payload in the exact snake_case shape the underlying oidc-provider persists (matching what RFC 7591 dynamic registration writes) and then evicts the provider's dynamic-client cache so the change takes effect immediately. Each operation emits an audit event: client.created, client.updated, client.deleted.

Enumeration relies on the adapter's optional list?() capability (an adapter that implements only the older listClients?() is still honored, delegating to list()). When the adapter implements neither, the service's canList is false and the page degrades gracefully — the create form still works, but the adapter-stored list is hidden with a notice rather than erroring.

Organizations

When the three organization tables are present, an Organizations view appears at ?view=orgs. See Organizations for the full route table and behaviour.

Organization management

The Organizations tab supports the full CRUD lifecycle:

  • Create — form with display name, slug, and optional logo URL.
  • Edit — rename and update slug/logo. The slug must be unique and URL-safe.
  • Members — list current members, change roles, and remove members.
  • Invitations — create email invitations and revoke pending ones.
  • Danger zone — delete the organization (cascades membership rows; does not delete member accounts).

Org-scoped settings (Settings tab in the org drawer)

Opening an org and selecting the Settings tab shows two cards that can be overridden per-organization:

CardSetting keyWhat it controls
Organization Policyorganizations_policyallowSelfCreate, invitationTtlHours, roles list
Roles Catalogroles_catalogCustom role definitions available within this org

Saving an org-scoped value creates a row in auth_settings with organization_id set to the org's ID. The resolution chain is org → global → lib default. See Runtime Settings — Org-scoped settings for details.

Signing keys

?view=keys is the operational view for the managed JWKS signing key. GET {prefix}/api/keys reports the current key's age alongside the effective rotation policy and when the next rotation is due, and POST {prefix}/api/keys/rotate performs a rotation on the spot — the same operation as node ace authkit:keys:rotate, with the same keep / retire semantics in the request body.

This view only means something when the JWKS is managed and has a persisted store: with an inline keyset there is nothing for AuthKit to rotate, and with a managed keyset and no store the key is regenerated every boot anyway. In both of those cases the endpoints answer 501 not_implemented rather than pretending. See Keystore vaults for the store options and Signing key rotation for the policy.

Settings page

?view=settings is the Runtime Settings view — the operational configuration that changes without a redeploy. Everything on it goes through one small HTTP surface rather than a route per control:

RouteAction
GET {prefix}/api/settingsRead every stored setting, plus a locked array naming the keys frozen by defineConfig.
GET {prefix}/api/settings?organizationId={id}The same, scoped to one organization.
PUT {prefix}/api/settings/:keyUpsert a setting. Body: { "value": <json> }.
DELETE {prefix}/api/settings/:keyDelete a setting — the effective value falls back to config, then to the lib default.

Both writes accept the same ?organizationId= query parameter, and both emit a settings.updated audit event carrying the key, the value (or action: "deleted"), source: "admin-console", and the organization id.

Two failure modes are worth knowing before you wire an operator up to this page:

  • No auth_settings table — every one of these endpoints answers 404 with capability_unsupported, and the view degrades to a notice telling you to run the migration. Runtime settings are an optional capability, not a hard requirement.
  • The key is locked by config — writes answer 423 Locked with setting_locked and lockedBy: "config". This is not an error to work around: it means the value was declared in config/authkit.ts, which by design outranks anything set at runtime. Remove it from defineConfig to hand control back to the console. See Config locks.

The sections on the page

The view is a stack of structured sections. Each section is one setting key, and the fields inside it map onto the fields of that key's stored JSON object — so saving the "Cadastro" section writes {"enabled": true} to the registration key, nothing more magical than that. A section whose key is locked renders read-only with a "defined via defineConfig" note.

SectionSetting keyFields
Login methodsauth_methodspassword, magicLink, passkey, forgotPassword, passkeyAutofill
Sign-upregistrationenabled
Email verificationrequire_verified_emailenabled, graceDays
Maintenancemaintenance_modeenabled, message
Lockoutlockoutenabled, maxAttempts, windowSec, baseLockoutSec, maxLockoutSec
Token TTLtoken_ttlaccessTokenSec, idTokenSec, refreshTokenSec

A few notes on the fields that are not self-explanatory:

  • auth_methods.forgotPassword depends on password: there is nothing to reset when password login is off, so the resolver treats it accordingly. magicLink and passkey additionally need the underlying capability (mail plus passwordless.magicLink, and a configured WebAuthn respectively) — the setting cannot switch on a method the code does not have wired up. See Security — Authentication methods.
  • require_verified_email.graceDays lets newly-registered accounts sign in without a verified email for N days (0 disables the grace window). It is a no-op when the account store does not implement the isEmailVerified capability. See Compliance — Email verification gate.
  • maintenance_mode blocks login, sign-up, and forgot-password for everyone whose roles do not match admin.roles, so an operator can still get in and turn it back off. Leaving message blank falls back to the built-in translated string. See Security — Maintenance mode.
  • lockout is the per-account brute-force lock (progressive backoff from baseLockoutSec up to maxLockoutSec), which is a different mechanism from the route throttles. See Account lockout.
  • token_ttl changes take effect immediately — the provider reads the TTLs through mutable holders rather than capturing them at boot. Session lifetime is not here; it lives in session_policy.

If a locked-out operator needs maintenance_mode turned off and the console itself is unreachable, the Admin REST API is the escape hatch — it writes the same settings with an API key instead of a session:

curl -X PUT https://idp.acme.com/api/authkit/v1/settings/maintenance_mode \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "value": { "enabled": false } }'

Keys the form does not cover

The runtime settings catalog is larger than the console's form. There are 23 known keys in all; the six sections above are the ones with a purpose-built UI. The rest are still fully usable — they are ordinary rows in auth_settings, reachable through the same PUT {prefix}/api/settings/:key, through the Admin REST API, and from the command line:

node ace authkit:settings:list                       # every stored setting
node ace authkit:settings:get password_policy        # read one
node ace authkit:settings:set sudo_mode '{"enabled":true,"graceMinutes":15}'
node ace authkit:settings:unset sudo_mode            # back to config / lib default

The keys without a form section are bot_protection, password_policy, password_history, password_expiration, session_policy, trusted_devices, notifications, security_notifications, email_change, rate_limit, admin_impersonation, organizations_policy, roles_catalog, sudo_mode, otp_lockout, account_expiration, and key_rotation.

Runtime Settings is the reference for all of them: the shape of each value, the precedence chain (org → global → config → lib default), the cache TTL, and the capability each one depends on.

organizations_policy and roles_catalog are the two org-scopable keys. They are the ones the org drawer's Settings tab edits, and the only ones for which ?organizationId= changes anything — every other key is global.

On this page