Clients & registration
Confidential and public clients, RFC 7591 Dynamic Client Registration, and scope management.
OAuth clients
- Multi-tenant: each client belongs to an environment.
- Per-client scope grants and multiple redirect URIs per client.
- Configurable client ID prefix (
CLIENT_KEY_PREFIX).

Confidential and public clients
token_endpoint_auth_method determines how a client authenticates at /token:
| Method | Secret? | Proof at /token |
|---|---|---|
client_secret_basic (default) | Yes (hashed secret generated) | Client secret |
client_secret_post | Yes | Client secret in the body |
none (public / PKCE-only) | No | PKCE code_verifier |
Public clients
Public clients have no secret — PKCE is the proof of possession at /token. This is the right
choice for SPAs, mobile apps, and MCP clients. client_credentials is rejected for public
clients. See the SPA / public client guide.
Dynamic Client Registration (RFC 7591)
POST /api/register lets clients self-register without an admin. It is a public endpoint,
CORS-enabled for browser-based MCP clients, and is how MCP clients (Claude, ChatGPT) connect.
It accepts:
redirect_uris(required, exact-matchhttps—http://localhostallowed for local clients, capped in count)token_endpoint_auth_method(defaultnone)grant_types(authorization_code/refresh_token)response_types(code)client_namescope(must be known scopes)
It returns client_id (plus client_secret only for confidential clients). Registered clients land
in the DCR_ENVIRONMENT_ID environment and are flagged is_dynamic; the admin Clients list
shows a Dynamic badge and a registration-type filter.
Abuse controls
DCR is protected by a per-IP daily rate limit (DCR_RATE_LIMIT_PER_DAY, default 10, counting
every attempt), exact redirect-URI matching (no wildcards), and DB-backed counters pruned by the
daily cron. Disable it entirely by leaving DCR_ENVIRONMENT_ID unset or setting
DCR_ENABLED=false.
See the MCP server with DCR guide for an end-to-end walkthrough.
Scopes
Default OIDC scopes are seeded on install: openid, profile, and email are created by the
schema/seed so OpenID Connect works out of the box.
openidis required to mint anid_tokenand to call/userinfo.profileandemailgate their respective claims.- Seeding only defines the scopes — an admin still grants them to each client, and the
client must request them at
/authorize(or request noscope, which defaults to all of that client's grants).
Additional capabilities:
- Custom scope definitions (admin dashboard → Scopes).
- Per-client scope allowlist.
- Scope propagation through token rotation.
Scope resolution
Requesting a scope a client wasn't granted fails with invalid_scope. In the
client library, prefer the OIDCScope constants and the scopes[] array
so a typo can't silently drop openid.