Documentation
OAuth ServerClients

Clients

Registering third-party applications, handling secrets and redirect URIs, and when to allow dynamic registration.

A client is an application allowed to ask your users for access. Registering one produces a client_id, optionally a client_secret, and the list of redirect URIs the flow may return to.

Public and confidential

The split is not about trust, it is about whether the app can keep a secret.

Confidentialhas a client_secretOptional

Runs on a server the developer controls. Sends client_secret on the token request. A web backend, a daemon, a CLI with a server component.

Publicno secretOptional

Runs where the user can read it — a browser SPA, a mobile app, a desktop app. Any secret shipped there is published. Authenticates with PKCE alone.

A single-page app is public even if the developer insists otherwise. Anything in the bundle is readable by anyone who opens devtools.

Both types run the same flow, and both need PKCE. The secret is an additional factor for clients that can hold one, not a replacement for the verifier.

Redirect URIs

Registered up front, matched exactly.

Registered:  https://acme.example.com/callback
Matches:     https://acme.example.com/callback
Rejected:    https://acme.example.com/callback/
Rejected:    https://acme.example.com/Callback
Rejected:    https://acme.example.com/callback?next=/home

No wildcards, no prefix matching, no substring. It is stricter than older OAuth providers and it is the single most valuable rule in the specification: a loose redirect match is how authorization codes get delivered to the wrong party.

Dynamic values that used to live in the redirect URI belong in state instead, which is returned untouched.

Register one URI per environment your client actually uses — http://localhost:3000/callback for development is fine and normal.

Registering from the Console

OAuth Server → Applications → New application. You provide:

NamerequiredOptional

Shown on the consent screen. Use the name users know the product by, not an internal codename.

Redirect URIsrequiredOptional

One or more, exact.

Logo URIoptionalOptional

Rendered on the consent screen. Helps a user recognise a familiar app.

Client URIoptionalOptional

The app's homepage, so a user can check who is asking.

The secret is displayed once, at creation. There is no way to read it back — regenerating is the only recovery, and it invalidates the old one immediately.

Registering programmatically

const { data, error } = await nulvless.auth.oauth.register({
  name: 'Acme Analytics',
  redirectUris: ['https://acme.example.com/callback'],
  logoUri: 'https://acme.example.com/logo.png',
  clientUri: 'https://acme.example.com',
});

Dynamic registration

RFC 7591 lets an application register itself, with no human in your Console. It is off by default, and the trade-off is worth stating plainly.

Turn it on when you want an open ecosystem — anyone can build against you without asking — or when you are serving MCP clients and agent tooling, where the registering party is software.

Leave it off when your integrations are partnerships. Every client that exists is a name that can appear on a consent screen next to your product, and an open endpoint means you do not control that list.

Open registration is an open door to your consent screen

Anyone who can register can create an app called "Account Security" with a convincing logo and send your users a link. Nothing is compromised technically — the user still has to approve — but the consent screen is a trust surface, and open registration means you no longer curate it. If you enable this, treat client names as untrusted content on your consent screen.

Rotating a secret

Applications → the app → Regenerate secret. The old one stops working immediately: there is no overlap window, so coordinate with whoever runs the client before you do it.

Rotate when someone with access leaves, when a secret has been in a log or a support ticket, or on whatever schedule your compliance program sets.

What users can revoke

A user who has granted access can take it back, and your application can show them that list:

const { data: grants } = await nulvless.auth.oauth.list();
await nulvless.auth.oauth.revoke(clientId);

Worth building into your account settings. "Which apps can see my data, and how do I stop one" is a question people ask after they have already granted something, and the answer should not be a support ticket.

On this page