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:
defineConfig({
// ...
admin: {
enabled: true,
roles: ['ADMIN'], // global roles allowed into /admin; default ['ADMIN']
},
})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.
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | boolean | false | Mounts the admin console group behind the admin guard. |
roles | string[] | ['ADMIN'] | Global roles that grant access to the console. An empty array falls back to ['ADMIN']. |
impersonation | boolean | true | Kill 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:
- the RFC 8693
urn:ietf:params:oauth:grant-type:token-exchangegrant registered on the OIDC provider — the mechanism by which an admin trades their own access token for one that acts as another account; - 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:
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:
// 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:
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/*withCache-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:
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);- an active account session (otherwise a redirect to the account login URL, default
/account/login, carrying areturn_to); - 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; - the signed-in account having at least one of
admin.roles(otherwise a redirect to the configuredaccountHome— default/account/security— so the existence of/adminis 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.
| Route | Notes |
|---|---|
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/overview | Dashboard metrics (total users, MAU, sessions, daily charts) |
GET {prefix}/api/users | List/search users — ?search&page&size, paginated { meta, data } |
GET {prefix}/api/users/:id | User detail |
POST {prefix}/api/users | Create user |
PATCH {prefix}/api/users/:id/roles | Update roles |
POST {prefix}/api/users/:id/disable | Disable account |
POST {prefix}/api/users/:id/enable | Enable account |
POST {prefix}/api/users/:id/reset-password | Send password reset email |
DELETE {prefix}/api/users/:id | Delete user (cascade + anonymize) |
GET {prefix}/api/users/:id/sessions | List a single user's active sessions |
POST {prefix}/api/users/:id/revoke-sessions | Disconnect a user from all devices (revoke all their sessions + grants) |
GET {prefix}/api/sessions | List active sessions/grants (all accounts) |
POST {prefix}/api/sessions/revoke-all | Revoke all sessions for an account |
GET {prefix}/api/clients | List adapter-stored OIDC clients |
POST {prefix}/api/clients | Create a dynamic client |
PATCH {prefix}/api/clients/:id | Edit a dynamic client |
DELETE {prefix}/api/clients/:id | Delete a dynamic client |
POST {prefix}/api/clients/:id/regenerate-secret | Regenerate client secret |
GET {prefix}/api/roles | List the roles catalog |
POST {prefix}/api/roles | Add a role to the catalog |
PATCH {prefix}/api/roles/:name | Update a catalog role |
DELETE {prefix}/api/roles/:name | Remove a catalog role |
GET {prefix}/api/orgs | List organizations |
POST {prefix}/api/orgs | Create an organization |
GET {prefix}/api/orgs/:id | Org detail |
PATCH {prefix}/api/orgs/:id | Update an organization (name, slug, logo) |
DELETE {prefix}/api/orgs/:id | Delete an organization |
POST {prefix}/api/orgs/:id/members | Add a member |
PATCH {prefix}/api/orgs/:id/members/:accountId | Change a member's role |
DELETE {prefix}/api/orgs/:id/members/:accountId | Remove a member |
POST {prefix}/api/orgs/:id/invitations | Create an email invitation |
DELETE {prefix}/api/orgs/:id/invitations/:invitationId | Revoke a pending invitation |
GET {prefix}/api/audit | Browse audit log — ?type&subject&page&size, paginated { meta, data } |
GET {prefix}/api/settings | Read all runtime settings (add ?organizationId= for org-scoped) |
PUT {prefix}/api/settings/:key | Upsert a setting — 423 when locked by config |
DELETE {prefix}/api/settings/:key | Delete a setting (reset to lib default) — 423 when locked |
GET {prefix}/api/keys | Managed signing-key status |
POST {prefix}/api/keys/rotate | Rotate the managed signing key |
GET {prefix}/api/impersonation/:userId | Get 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.successin 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:
| Field | Source |
|---|---|
| Browser | Parsed from the User-Agent string (e.g. Chrome 124) |
| OS | Parsed from the User-Agent string (e.g. macOS) |
| IP address | Request IP at login |
| Location | Resolved via the host's resolveGeo hook (optional) |
To add geolocation, plug in a resolveGeo hook in defineConfig:
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_idwhen omitted; for confidential clients (tokenEndpointAuthMethod !== 'none') it generates aclient_secretand 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_secretfor 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:
| Card | Setting key | What it controls |
|---|---|---|
| Organization Policy | organizations_policy | allowSelfCreate, invitationTtlHours, roles list |
| Roles Catalog | roles_catalog | Custom 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:
| Route | Action |
|---|---|
GET {prefix}/api/settings | Read 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/:key | Upsert a setting. Body: { "value": <json> }. |
DELETE {prefix}/api/settings/:key | Delete 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_settingstable — every one of these endpoints answers 404 withcapability_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_lockedandlockedBy: "config". This is not an error to work around: it means the value was declared inconfig/authkit.ts, which by design outranks anything set at runtime. Remove it fromdefineConfigto 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.
| Section | Setting key | Fields |
|---|---|---|
| Login methods | auth_methods | password, magicLink, passkey, forgotPassword, passkeyAutofill |
| Sign-up | registration | enabled |
| Email verification | require_verified_email | enabled, graceDays |
| Maintenance | maintenance_mode | enabled, message |
| Lockout | lockout | enabled, maxAttempts, windowSec, baseLockoutSec, maxLockoutSec |
| Token TTL | token_ttl | accessTokenSec, idTokenSec, refreshTokenSec |
A few notes on the fields that are not self-explanatory:
auth_methods.forgotPassworddepends onpassword: there is nothing to reset when password login is off, so the resolver treats it accordingly.magicLinkandpasskeyadditionally need the underlying capability (mail pluspasswordless.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.graceDayslets newly-registered accounts sign in without a verified email for N days (0disables the grace window). It is a no-op when the account store does not implement theisEmailVerifiedcapability. See Compliance — Email verification gate.maintenance_modeblocks login, sign-up, and forgot-password for everyone whose roles do not matchadmin.roles, so an operator can still get in and turn it back off. Leavingmessageblank falls back to the built-in translated string. See Security — Maintenance mode.lockoutis the per-account brute-force lock (progressive backoff frombaseLockoutSecup tomaxLockoutSec), which is a different mechanism from the route throttles. See Account lockout.token_ttlchanges take effect immediately — the provider reads the TTLs through mutable holders rather than capturing them at boot. Session lifetime is not here; it lives insession_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 defaultThe 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.