Documentation
ReferenceJavaScript SDKSessionManager

SessionManager

The token lifecycle the browser client runs for you, and how to drive it directly.

The browser client keeps a session alive without you doing anything: it stores the tokens, notices when the access token is about to expire, exchanges the refresh token for a new pair, and raises an event when it cannot.

You never call it directly — the browser client owns it. auth.getAccessToken() and auth.refreshSession() are the public doors and both go through here. This page is for when you need to know what is happening underneath.

Browser only. createServerClient has no session manager, because a server holds no single user's session to keep alive.

What is stored

access_tokenstringRequired

What goes on the wire.

refresh_tokenstringOptional

What buys the next one. Without it a session cannot survive expiry.

expires_atnumberRequired

An absolute epoch timestamp, computed on save as now + expires_in. Defaults to one hour when the server does not say.

userUserOptional

Carried so the client can answer "who is signed in" without a request.

Note it stores an absolute expiry, not the relative expires_in the server returns. A stored session is therefore still correct after the tab was asleep for an hour, where a countdown would not be.

getValidToken

getValidToken() → string | null

Returns a usable token, refreshing first if it is close to expiring.

const token = await nulvless.auth.getAccessToken(); // goes through this

The rule is a 90-second buffer: if the token expires in less than that, it is refreshed before being handed back. That margin exists so a token is never handed out and then rejected mid-flight — the request would have failed between your check and the server's.

Concurrent callers share one refresh. Ten requests that all find the token stale produce one exchange, and the other nine wait for its result — across tabs too, so two open tabs do not race each other for the same refresh token.

Returns null in three cases, and they are worth telling apart:

no sessionnullOptional

Nothing stored. Nobody is signed in.

no refresh tokennullOptional

A session exists but cannot renew itself. Raises unauthorized.

refresh refusednullOptional

The server refused the refresh token — a 401, or a token that is unknown, already used, issued elsewhere, or belongs to a banned user. The stored session is cleared and unauthorized is raised.

A refresh that merely could not complete — no network, a timeout, a 429, a 5xx — is retried up to three times with backoff. If it still fails, null comes back but the session is kept, so a flaky connection never signs anyone out.

This is why holding on to a token string defeats the mechanism. Ask per request:

await fetch('/api/projects', {
  headers: { Authorization: `Bearer ${await nulvless.auth.getAccessToken()}` },
});

refreshSession

auth.refreshSession() → string | null

Renews the access token now, regardless of how much life it has left, and shares the attempt with any renewal already in flight. Returns the new token, or null when the session could not be recovered.

const token = await nulvless.auth.refreshSession();

Reach for it after something changed server-side that the current token does not carry yet — new scopes after accepting an invitation, for example.

forceRefresh

forceRefresh({ deauthOnFailure }) → string | null

What refreshSession calls, and what the client calls itself after a 401 or a 403 forbidden_resource_access. Refreshes regardless of how much life the current token has — unless another tab already replaced it, in which case that token is returned without a second exchange.

deauthOnFailurebooleanOptionaldefault true

What happens when the refresh itself fails.

true

Clear the stored session and raise unauthorized. The right behaviour after a 401 — the session really is over.

false

Leave the session intact and just return null. For a soft refresh, where the original failure was about permissions rather than identity.

The distinction matters. A 403 forbidden_resource_access means the user is signed in and not allowed — deauthenticating them there would sign someone out for clicking the wrong button. Pass deauthOnFailure: false so the caller can surface the real error.

Scheduled renewal

With autoRefresh on (the default), the browser client does not wait for a request to find the token stale. Every 30 seconds — and as soon as a hidden tab becomes visible again — it checks the stored session and renews it once it is inside the 90-second buffer.

  • Hidden tabs skip the check. A background tab does not keep a session warm that nobody is looking at.
  • One tab renews at a time. Tabs of the same project coordinate through the Web Locks API where it exists; a tab that finds the lock taken skips that tick.
  • Every tab hears about it. The new token is broadcast over BroadcastChannel, and each tab emits token-refreshed.

Turn it off with createClient(url, key, { autoRefresh: false }) when your host controls renewal itself. Renewal on use still happens.

Call auth.dispose() when you throw a client away — tests, hot reload, a micro-frontend unmounting. It stops the timer and closes the cross-tab channel.

Callbacks

Three hooks drive everything above. The browser client wires them to the events you subscribe to, so subscribing is normally enough.

setRefreshFn(refresh_token) => Promise<Result>Optional

How to exchange a refresh token. Set by the client at construction — it is injected afterwards rather than passed in, to avoid a circular dependency between the manager and the HTTP client.

setOnTokenRefreshed(auth) => voidOptional

Fires after every successful refresh. Becomes the token-refreshed event.

setOnUnauthorized(failed) => voidOptional

Fires when the session cannot be renewed. Becomes the unauthorized event, and carries the session that failed so a multi-account app can drop the right entry.

save, get, clear

save(auth) · get() · clear()

The storage primitives. save computes expires_at and writes through the configured storage adapter; clear removes the active session and nothing else — the accounts store is untouched, which is what lets deselect drop a session and still switch back to it.

signOut calls clear for you, and only after the server confirms — so a failed sign-out does not leave the client locally signed out and remotely alive.

Next

On this page