Concepts

Applications

How to register and manage OAuth 2.0 client applications in auth-service.

An application in auth-service is an OAuth 2.0 client registration combined with an access policy, an RBAC model, and subscription plans. Every application has a unique slug that doubles as its OAuth client_id.

Application schema

FieldTypeDefaultDescription
idUUIDautoPrimary key
namestringrequiredDisplay name (max 100 chars)
slugstringrequiredURL-safe identifier, lowercase alphanumeric + hyphens (max 64 chars). Used as client_id. Cannot be changed after creation
descriptionstring—Optional description (max 500 chars)
isActivebooleantrueWhether the application is active. A disabled application is also disabled on its OAuth client and stops issuing tokens
isPublicbooleanfalsePublic (no client secret) vs confidential client. Fixed at creation
isSystembooleanfalseInfrastructure application provisioned by scripts (for example the LLM gateway): hidden from the admin list unless "Show system applications" is on, and refused deletion (APP_007). Only a superadmin can set or clear it
skipConsentbooleanfalseSkip the OAuth consent screen
isMfaRequiredbooleanfalseToken issuance is refused for users who have not enabled two-factor authentication
billingMode"none" | "delegated""none"none: no billing pages. delegated: auth-service serves the subscribe, invoices and usage pages for the application
allowRegisterbooleanfalseAllow new-user self-registration via this app. Accounts can only be created through an application that allows it
allowedScopesstring["openid","profile","email"]Scopes the app is permitted to request
redirectUrisstring[]Allowed OAuth redirect URIs
urlstring—Application URL. Also registered as this application's protected resource (RFC 8707 audience)
allowedResourcesstring[]URLs of other applications (protected resources, e.g. an LLM gateway) this application may request access tokens for via resource. Each entry must be an already registered protected resource (the url of another application) and must not be this application's own url
iconstring—Icon URL (displayed in the admin SPA)
enabledSocialProvidersstring | nullnullPer-app social providers. null inherits all globally configured providers.
enableEndSessionbooleanfalseAllow the application to call /api/auth/oauth2/end-session, which ends the SSO session for all applications. Also puts sid in the application's ID tokens. See Managing applications and Sessions and sign-out
postLogoutRedirectUrisstring[]Whitelist for post_logout_redirect_uri (exact match). When an application is created with none and has a url, the list is seeded with <url>/
backchannelLogoutUristring | nullnullOIDC Back-Channel Logout endpoint of the application. The admin API accepts public https:// URLs only
backchannelLogoutSessionRequiredbooleanfalseDeclares that the application requires sid in the logout token (auth-service always sends it)
metadataobject{}Flat string-to-string map (values up to 256 chars, keys are identifiers; reserved claim names such as sub or aud are refused). Emitted under the client_attrs claim of the application's tokens, client_credentials tokens included

enableEndSession, postLogoutRedirectUris and the two back-channel fields are stored on the BetterAuth OAuth client record and merged into every application payload returned by the admin API.

Machine scopes

A confidential application can use the client_credentials grant. The scopes it may request are set apart from the application fields, through PUT /api/admin/applications/:id/machine-scopes (superadmin only): m2m (default), wallet:read, wallet:debit and billing:read. The two wallet scopes let a machine client read and debit any user's wallet, which is why they are not part of the regular update. billing:read gives read access to the subscription, usage and invoices of users of the calling application (/api/app/billing/*). Removing a scope revokes the client's existing tokens. Public applications cannot use this grant.

Public vs confidential clients

PKCE (code_challenge / code_verifier, method S256) is required on every authorization request, for public and confidential clients alike.

When isPublic is true:

  • No client secret is generated.
  • token_endpoint_auth_method is set to none in the OAuth client record.
  • The client is limited to the authorization_code and refresh_token grants.

When isPublic is false (default):

  • A random 32-byte hex secret is generated at creation time.
  • The secret is stored hashed (SHA-256 base64url) and returned once in the creation response.
  • The client authenticates at the token endpoint with HTTP Basic (client_secret_basic) by default, or with the secret in the form body (client_secret_post) when the instance sets TOKEN_ENDPOINT_AUTH_METHOD=post (see Compatibility profiles). The grants are authorization_code, refresh_token and client_credentials.

Bootstrap macros

When a new application is created via POST /api/admin/applications, auth-service automatically:

  1. Creates two default roles: user (default) and admin.
  2. Creates a default free subscription plan.
  3. Assigns all existing superadmin users to the application with the admin role and the free plan.
  4. Assigns the caller who created the application with the admin role and the free plan, so an admin keeps access to the application it just created. A superadmin caller is covered by step 3 and is never attached twice.

Admin API endpoints

All application management endpoints require an admin or superadmin session.

List applications

GET /api/admin/applications

Returns all applications ordered by creation date, system applications included (the admin interface hides them unless "Show system applications" is on). Each application also carries enableEndSession, postLogoutRedirectUris, backchannelLogoutUri and backchannelLogoutSessionRequired.

{
  "applications": [
    {
      "id": "...",
      "name": "My App",
      "slug": "my-app",
      "isActive": true,
      "isPublic": false,
      ...
    }
  ]
}

Create an application

POST /api/admin/applications
{
  "name": "My App",
  "slug": "my-app",
  "description": "Optional description",
  "isPublic": false,
  "allowedScopes": ["openid", "profile", "email", "roles"],
  "redirectUris": ["https://app.example.com/callback"]
}

The response (201) is { application, clientId, clientSecret }. clientSecret contains the plaintext secret and is absent for public applications. Store it immediately — it cannot be retrieved afterwards. A duplicate slug is refused with APP_003.

Get an application

GET /api/admin/applications/:id

Update an application

PATCH /api/admin/applications/:id

All fields except slug and isPublic can be updated. name, allowedScopes, skipConsent, redirectUris, isActive, enableEndSession, postLogoutRedirectUris, backchannelLogoutUri and backchannelLogoutSessionRequired are mirrored onto the underlying BetterAuth oauthClient record. Changing isSystem requires a superadmin (AUTH_011 otherwise).

Delete an application

DELETE /api/admin/applications/:id

System applications cannot be deleted (APP_007). Otherwise the application is removed and responds 204: its dependent records (roles, permissions, plans, user access, consumption data) are cascade-deleted, its OAuth client and protected-resource links are removed, and its Stripe catalogue is archived rather than deleted. The delete route does not check for active subscriptions (the APP_004 code is defined in the registry but never raised).

Rotate client secret

POST /api/admin/applications/:id/rotate-secret

Generates a new random secret, hashes it, stores it, and returns { clientSecret } once. The old secret is invalidated immediately and every token issued to the client is revoked. Public applications are refused (APP_001).

Managing user access

Grant access to a user

POST /api/admin/applications/:id/users
{
  "userId": "...",
  "roleId": "...",
  "subscriptionPlanId": "...",
  "reason": "..."
}

roleId, subscriptionPlanId and reason are optional. Without roleId the application's default role is assigned; without subscriptionPlanId its default plan is assigned. Answers 201 { ok: true }. POST /api/admin/applications/:id/users/bulk grants many users at once (an organizationId or a list of userIds).

List users with access

GET /api/admin/applications/:id/users

Update a user's access

PATCH /api/admin/applications/:id/users/:userId
{
  "isActive": true,
  "roleId": "...",
  "subscriptionPlanId": "..."
}

expiresAt and reason are also accepted; reason is required when subscriptionPlanId or expiresAt is present. roleId replaces every role the user holds in the application (null removes them). Setting isActive to false also revokes the user's OAuth tokens for this application.

Revoke access

DELETE /api/admin/applications/:id/users/:userId

Revokes the user's OAuth tokens for the application, then deletes the user's roles, subscription, consumption data and access record for it. Answers 204.

Copyright © 2026