How Kiavi works
Deze inhoud is nog niet vertaald.
You don’t need to read this to ship, the quickstart is enough. This page exists for the day you hit something surprising and want to understand why the SDK behaves the way it does.
The trust model
Section titled “The trust model”Kiavi separates three roles:
- The auth server, the only thing that ever sees user passwords, social provider tokens, or session refresh tokens. Hosted by us in Paris, isolated per app.
- Your client app (web or mobile), never sees passwords. Sees a short-lived access token (a JWT) and either a session cookie (web, same-domain) or a refresh token in secure storage (mobile).
- Your backend, never sees passwords or refresh tokens. Verifies the access token’s signature against the auth server’s public JWKS and trusts the claims inside it.
This is plain OAuth 2.0 / OpenID Connect. There’s nothing exotic in the wire protocol. The SDKs are thin clients that handle PKCE, redirects, and silent refresh so you don’t have to.
Two server modes
Section titled “Two server modes”Every Kiavi auth server runs in one of two modes. The browser SDK reads /.well-known/kiavi-auth.json on first use and picks the right flow automatically, you don’t configure it in your app code. Which mode an instance uses is decided at deploy time by whether the app has a custom domain configured in the management UI.
Cookie mode (custom domain, recommended)
Section titled “Cookie mode (custom domain, recommended)”Your app at yourapp.com talks to an auth server at auth.yourapp.com. Because both share yourapp.com, the auth server sets a session cookie scoped to .yourapp.com that the app can see directly. The SDK reads the session from the server’s session_endpoint and mints JWTs via the better-auth jwt() plugin, no PKCE round trip needed.
This is the recommended production setup whenever a customer has their own domain. The reasons:
- Stronger session handling. The session lives in an
HttpOnly,SameSitecookie managed by the auth server, never visible to JavaScript on the app side. - Fewer moving parts. No authorization code, no PKCE verifier, no cross-origin refresh dance, the cookie just works.
- Better UX. The user stays on
*.yourapp.comfor the entire flow. The login page feels like part of the app, not a third-party service.
Kiavi’s deploy worker switches to cookie mode automatically when an app has a custom domain configured: auth.your-domain.com is provisioned, the SDK reads cookie-mode endpoints from the well-known config, and you don’t need to do anything else.
Exchange mode (Kiavi-subdomain default)
Section titled “Exchange mode (Kiavi-subdomain default)”Your app at yourapp.com talks to the auth server at yourapp.kiavi.eu. Standard PKCE authorization-code flow. The browser SDK redirects to the authorize endpoint, the auth server bounces back with ?code=&state= on the redirect URI, and the SDK exchanges the code for a session in its constructor. Refresh happens via a Partitioned cookie scoped to the auth server.
This is the mode you get out of the box on the *.kiavi.eu subdomain that’s provisioned for every new app. It’s also the mode mobile apps always use, since there’s no shared cookie scope between a native app and a web auth server.
If you want to upgrade to cookie mode, configure a custom domain for your app in the management UI and redeploy. Your SDK code doesn’t change, the well-known config flips and the SDK picks the new flow on next page load.
How silent refresh works
Section titled “How silent refresh works”Refresh is lazy, not timer-driven. There is no background interval ticking down to a renewal. Instead:
- Each
getAccessToken()call inspects the current token’s expiry. - If it’s within 30 seconds of expiring, the SDK kicks off a refresh before returning.
- Concurrent callers share a single in-flight refresh promise. A burst of API calls never fans out into multiple
/refreshrequests.
This means you should call getAccessToken() inline on every API request. It’s cheap, it handles refresh for you, and it never wastes a network round trip. Do not cache the result. Do not wrap it in your own retry / timer / interval logic, you’ll just fight the SDK.
If refresh fails for any reason other than unauthenticated (e.g. the user is offline), the saved refresh token / cookie is preserved. The next call retries. Only a 401 from /refresh clears local state, because that’s the server telling you the chain has been revoked.
What’s in the access token
Section titled “What’s in the access token”Verified JWTPayload (returned by kiavi-js’s verifyToken()):
type JWTPayload = { sub: string // user ID email: string emailVerified?: boolean name?: string | null app_id?: string app_slug?: string auth_method?: string iss: string aud: string | string[] exp: number iat: number}These claims are trusted because the JWT signature was verified against the auth server’s JWKS. Use them directly, don’t make a second round-trip to look the user up unless you need fields that aren’t in the token.
Where state lives
Section titled “Where state lives”| Where | What | Cleared by |
|---|---|---|
Browser sessionStorage | Cached access token, PKCE verifier | signOut(), tab close |
| Auth server (Partitioned cookie) | Refresh token (exchange mode) | signOut(), server-side revoke |
| iOS Keychain / Android Keystore | Refresh token (React Native) | signOut(), app uninstall |
| Auth server database | User account, sessions, factors | Account deletion, server-side revoke |
The client SDKs never write tokens to localStorage or unscoped cookies, and you shouldn’t either. All session storage is owned by the SDK.
Passkeys and the auth policy
Section titled “Passkeys and the auth policy”Apps configure an auth policy that controls how strongly users must authenticate. The browser SDK exposes the active policy via getAuthPolicy():
'passkey_required', every user must have a passkey enrolled. The hosted login flow forces enrollment on first sign-in.'passkey_preferred', passkeys are offered but not required. Users can sign in with email codes, social providers, or other configured methods.
You generally don’t need to read the policy in your code, the hosted pages enforce it. Only reach for getAuthPolicy() when you need UI that adapts (e.g. a settings page that shows a “Set up your passkey” prompt only on passkey_preferred).
Where to next
Section titled “Where to next”- Quickstart, the four-step integration.
- @kiavi/kiavi-browser, full reference for the web SDK.
- @kiavi/kiavi-react-native, full reference for the mobile SDK.
- @kiavi/kiavi-js, full reference for the server SDK.