Five steps and one callback handler. The examples are Next.js App Router, but this is standard OAuth 2.0 with PKCE — any library that speaks it will work unchanged.
Create an account, then add a client from your dashboard. Register the redirect URI your app will use — and the localhost variant too, since you will want it for development.
You get a client ID and a client secret. Put them somewhere safe now.
The client secret is a server-side credential. Never ship it to the browser, and never put it in a variable prefixed NEXT_PUBLIC_.
In your dashboard, upload your logo and set your colors. The logo editor lets you crop, zoom and reposition, so a wide wordmark and a square icon both end up looking deliberate.
Open the live preview before writing any code — it is the real page your users will see, with your branding already applied.
On a white-label plan you can point your own subdomain at it, so the address bar reads login.yourcompany.com instead of ours.
Generate a state and a PKCE verifier, stash both in short-lived HttpOnly cookies, and redirect. The state is how you know the callback you receive is the one you started.
PKCE is optional if you send a client secret, but there is no reason to skip it. With a challenge present, an intercepted authorization code is useless without the verifier.
Verify the state, exchange the code at /api/oauth/token, and store the tokens in HttpOnly cookies. The token endpoint is standard OAuth — snake_case keys, form-encoded or JSON body, credentials in the body or as HTTP Basic.
If you get invalid_grant: redirect_uri mismatch, the URI in the exchange must match the one you used at authorize. Matching is on scheme, host and path — query parameters are free, the path must be exact.
Profile fields live at the UserInfo endpoint. If all you need is “is this session still good”, the verify endpoint is cheaper and tells you when to rotate.
Refresh rotates. The token you send is consumed and the response carries its replacement — write the new one before you respond to your user, or a crash in between will sign them out.
From here you might want bulk import to bring an existing membership over, or the error and rate-limit tables for hardening your handler.