Skip to content

Session Lifecycle

This page is the complete specification of how a session lives, refreshes, expires, and ends, including multi-tab behavior and how your UI learns about every change.

Session states

StateMeaningHow you observe it
authenticatedValid access + refresh tokens heldsdk.auth.isAuthenticated() === true; onAuthStateChanged(user)
refreshingAccess token expired/near-expiry; refresh in flightNothing; invisible to the UI; requests queue
expiredRefresh failed (invalid/revoked/rotated token)session-expired event + onAuthStateChanged(null)
logged-outUser (or SDK) ended the session explicitlyonAuthStateChanged(null)

Token refresh rules

  • Single-flight: concurrent requests needing a refresh share ONE refresh call. Parallel 401s never trigger parallel refreshes.
  • Timeout: the refresh call has a bounded timeout (default 10 s). One slow connection can never stall every queued request.
  • Rotation: every refresh returns a NEW refresh token; the old one is invalidated immediately.
  • Reuse detection: if a refresh token that was already rotated is presented again, the backend revokes the session family (every session descended from the same original login on all devices) and the SDK fires session-expired. This makes a stolen refresh token a detectable, self-limiting event. (Normal logout, by contrast, revokes only the current session's tokens; see Logout semantics below.)
  • Retry cap: after a successful refresh, the original request is retried at most once. A second 401 is terminal.
text
GIVEN any authenticated request and an expired access token
WHEN the response is 401
THEN exactly one refresh is performed (shared across all concurrent 401s)
     and the original request is retried once.

GIVEN a refresh failure caused by the TOKEN (invalid, revoked, expired, or reused)
WHEN the refresh result is known
THEN the SDK clears the session and fires session-expired.

GIVEN a refresh failure caused by the TRANSPORT (timeout or network error)
WHEN the refresh result is known
THEN the session is RETAINED, every queued request receives a typed
     TimeoutError / NetworkError, and the caller may retry later.

One rule everywhere: token-level refresh failures end the session; transport failures do not. A flaky network never logs your users out.

UnauthorizedError vs SessionExpiredError: they are two layers of the same fact. When a token-level refresh failure ends the session, each queued request rejects with UnauthorizedError (the request-level outcome), while SessionExpiredError + the session-expired event are the app-level notification (fire once, react once; route to login).

The session-expired event

javascript
const unsubscribe = sdk.auth.onSessionExpired(() => {
  // Session ended server-side or refresh failed irrecoverably.
  // Route the user to login. Do NOT keep showing authenticated UI.
});

Fired when:

text
GIVEN an authenticated session
WHEN the refresh token is invalid, revoked, expired, or detected as reused
THEN the SDK clears all local session state and fires
     onSessionExpired() AND onAuthStateChanged(null) exactly once,
     in every open tab.

Transport failures (timeout, network error) never fire session-expired; they surface typed errors to the caller and the session is retained. See State & Security § Transport Policy.

Multi-tab coordination

Tabs of the same origin coordinate over a BroadcastChannel:

  • One tab's refresh rotates the token ONCE for all tabs; the other tabs adopt the new token.
  • A logout in any tab logs out every tab.
  • A session-expired in any tab expires every tab.
text
GIVEN two open tabs sharing a session
WHEN Tab A refreshes or logs out
THEN Tab B receives the new session (or the logout) without a network call,
     and never attempts to use a token the other tab already rotated.

Logout semantics

javascript
await sdk.auth.logout();
  • Always attempts server-side revocation of the current session's tokens (its access and refresh token); even when the access token is already expired (the refresh token is used to revoke).
  • Logging out on one device does not end the user's other-device sessions: each login has its own token pair. Revoking everything everywhere is what a password reset does; a whole session family is only revoked by the backend's reuse detection (see above).
  • If the backend call fails, local state is still cleared and onAuthStateChanged(null) fires; the user is never stuck "unable to log out".
  • Logout propagates to all tabs (see above).
text
GIVEN an authenticated session (access token may be expired)
WHEN logout() is called
THEN the SDK attempts revocation with the refresh token,
     clears all local session state, fires onAuthStateChanged(null) in all tabs,
     and resolves even if the revocation call fails.

Session restore on page load

If a stored refresh token exists, the SDK silently restores the session on initialization and fires onAuthStateChanged with the restored user. Because restore is asynchronous, await the readiness signal before rendering protected routes:

javascript
const authenticated = await sdk.auth.sessionReady; // true = session restored
sdk.auth.tokenExpiry;                              // epoch ms the access token expires

Silent refresh and your listeners

  • Login, restore, and any user-object change fire onAuthStateChanged with the new user.
  • A silent refresh with an unchanged user fires NO event; tokenExpiry advances, listeners are not called, components do not re-render.
text
GIVEN an authenticated session
WHEN a silent refresh completes and the user object is unchanged
THEN tokenExpiry is updated and NO onAuthStateChanged event fires.

Your onAuthStateChanged callback is the single source of truth for the current user; it will not silently drift out of date, and it will not spam you on routine refreshes.

Storage keys

The SDK persists session state in localStorage under environment-namespaced keys, so a sandbox app and a production app on the same origin never share a session:

KeyEnvironmentContents
innovede_session_sandbox_refresh_tokensandboxCurrent refresh token
innovede_session_staging_refresh_tokenstagingCurrent refresh token
innovede_session_production_refresh_tokenproductionCurrent refresh token

Access tokens are held in memory only and are never persisted.

Proactive refresh

Before any authenticated call, if the access token expires within the configured refresh window, the SDK refreshes silently first; long-lived tabs never send a dead token. See State & Security § Proactive Silent Refresh.

Clock skew

Proactive refresh applies a 30-second skew tolerance, so a client clock running slightly fast never sends a token the server considers expired. See State & Security § Transport Policy.