Skip to content
rw3iss Auth

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_clientsm2m_clients
Answers”Who is this person?""Which service is calling?”
Created byAn admin or any signed-in memberA system_admin, only
EndpointPOST /admin/oauth/clients · POST /oidc/clientsPOST /admin/m2m-clients
Grantsauthorization_code, refresh_token, and — when an admin says so — client_credentialsclient_credentials only
A user is presentYes. The whole flow is a person signing inNo. There is no user at all
Token subject (sub)The user’s UUIDThe client id
Token shapeNormal user access token + an RS256 id_tokentoken_type: "service", no user fields
Migration025 + 028012

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 toUseNotes
Add “Sign in with {Provider}” to a web app with a backendoauth_clients, confidential (public: false)Keeps a secret. authorization_code + PKCE + refresh_token.
Sign users into an SPA or mobile appoauth_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 userm2m_clientsclient_credentials. Admin-only to create.
Give an existing interactive application a service identity toooauth_clients with client_credentials added to grant_typesAdmin-only. Confidential clients only. New — see below.
Ship a browser widget that reads public dataNeither — or a public clientA 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 deploymentPOST /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 itsystem_admin or super_adminAny authenticated member
client_idYou choose itThe server mints it — name-slug + 6 random bytes
Scopes availableAll 8, including civic:* and offline_accessopenid, profile, email only
grant_typesSettable + validatedFixed to authorization_code + refresh_token
trusted (skip consent)SettableNever
require_pkceSettableForced true
app_codeSettableNever
Volume capNone10 per account, 10 creates/hour
Ownerowner_user_id is NULLThe caller’s user id
Visible to the owner UINoYes

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.