Application types
An “application” in auth-server is not one thing. There are two separate registries, they live in different tables, they are created through different endpoints, and they answer different questions:
oauth_clients | m2m_clients | |
|---|---|---|
| Answers | ”Who is this person?" | "Which service is calling?” |
| Created by | An admin or any signed-in member | A system_admin, only |
| Endpoint | POST /admin/oauth/clients · POST /oidc/clients | POST /admin/m2m-clients |
| Grants | authorization_code, refresh_token, and — when an admin says so — client_credentials | client_credentials only |
| A user is present | Yes. The whole flow is a person signing in | No. There is no user at all |
Token subject (sub) | The user’s UUID | The client id |
| Token shape | Normal user access token + an RS256 id_token | token_type: "service", no user fields |
| Migration | 025 + 028 | 012 |
Both registries share one token endpoint, POST /oauth/token, because
that is what OIDC discovery advertises. Which registry serves a request is
decided by the client id: the OIDC handler looks the id up in
oauth_clients first and hands anything it does not recognise to the m2m
handler (routes.go,
POST /oauth/token).
I want to… → use…
| I want to | Use | Notes |
|---|---|---|
| Add “Sign in with {Provider}” to a web app with a backend | oauth_clients, confidential (public: false) | Keeps a secret. authorization_code + PKCE + refresh_token. |
| Sign users into an SPA or mobile app | oauth_clients, public (public: true) | No secret exists to steal. PKCE is mandatory and is the only thing binding a code to you. |
| Let a cron job / CI runner / backend service call the API with no user | m2m_clients | client_credentials. Admin-only to create. |
| Give an existing interactive application a service identity too | oauth_clients with client_credentials added to grant_types | Admin-only. Confidential clients only. New — see below. |
| Ship a browser widget that reads public data | Neither — or a public client | A browser cannot hold a secret. If it needs the user, register a public client and use PKCE. If it does not, do not issue a credential at all. |
| Register my own application against someone else’s deployment | POST /oidc/clients (self-service) | Any signed-in member. Identity scopes only, max 10 per account. |
The two ways to get an oauth_clients row
Same table, same runtime behaviour, very different capabilities at registration. The self-service surface can do strictly less, which is what makes it safe to open to every member.
Admin (/admin/oauth/clients) | Self-service (/oidc/clients) | |
|---|---|---|
| Who may call it | system_admin or super_admin | Any authenticated member |
client_id | You choose it | The server mints it — name-slug + 6 random bytes |
| Scopes available | All 8, including civic:* and offline_access | openid, profile, email only |
grant_types | Settable + validated | Fixed to authorization_code + refresh_token |
trusted (skip consent) | Settable | Never |
require_pkce | Settable | Forced true |
app_code | Settable | Never |
| Volume cap | None | 10 per account, 10 creates/hour |
| Owner | owner_user_id is NULL | The caller’s user id |
| Visible to the owner UI | No | Yes |
The NULL owner is load-bearing, not incidental. Every self-service query
carries owner_user_id = $caller inside the statement, and
= $caller never matches NULL — so admin-created clients, including the
trusted first-party one, are invisible and untouchable through the
self-service endpoints without a single extra check
(store_selfservice.go).
What changed recently
If you integrated before 2026-08, three things behave differently now.
Read next
- Client properties — every column
on
oauth_clients, what it does, its default, and where it is enforced. - Registering an application — the admin
and self-service flows, with working
curl. - The grants — a worked example per grant.
- Scopes — all eight, and the two sensitive ones.
- Users, apps and tenancy — how an operator decides which users belong to which application, and what is not enforced.
- Redirect URIs — the exact-match rule the whole flow rests on.
- OIDC provider — the wire
protocol: discovery, JWKS,
id_tokenclaims.