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_secretOptionalRuns 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 secretOptionalRuns 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=/homeNo 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:
NamerequiredOptionalShown on the consent screen. Use the name users know the product by, not an internal codename.
Redirect URIsrequiredOptionalOne or more, exact.
Logo URIoptionalOptionalRendered on the consent screen. Helps a user recognise a familiar app.
Client URIoptionalOptionalThe 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.