🎉 Native Google & Apple sign-in is here → read the guide

Sessions

Once a user signs in, you get a session — proof they’re signed in, made of an access token and a refresh token. The SessionManager looks after that session for you: it holds the current session, refreshes the access token before it expires, and tells your UI whenever the login state changes. So you never have to juggle tokens or expiry timers by hand.

Why the refresh matters: the access token expires quickly (minutes), which limits the damage if it ever leaks. The longer-lived refresh token is used to get a new access token when the old one runs out — quietly, in the background, so the user stays signed in.

Observing session state

sessionState is a flow you can collect to react to login changes — show the home screen when authenticated, show the sign-in screen when not. The four cases below are the complete set of states.

sessionManager.sessionState.collect { state ->
    when (state) {
        is SessionState.Authenticated -> println("User: ${state.session.user.id}")
        is SessionState.Expired -> println("Session expired, refreshing...")
        SessionState.NotAuthenticated -> println("Signed out")
        SessionState.Loading -> println("Loading...")
    }
}

A failed refresh does not automatically mean Expired. If the refresh fails for a transient reason — no connectivity, a 5xx, or a 429 rate limit — the refresh token is still valid, so the session stays Authenticated and the manager retries shortly. It only flips to Expired on a genuine invalidation (an expired or already-used refresh token). So Expired means “sign in again,” not “the network hiccupped” — you don’t need to bounce users to the login screen on a flaky connection.

Persisting across restarts

By default the session lives only in memory (InMemorySessionStorage), so it’s gone when the app closes — the user would have to sign in again on every launch. Persistence fixes that: write the session somewhere durable so it survives a restart.

That “somewhere” is your choice. The SDK deliberately bundles no secure-storage dependency, so it doesn’t force a particular one on you — instead you plug in the right store for each platform: Keychain (Apple), EncryptedSharedPreferences or DataStore (Android), localStorage (Web), a file (desktop). You implement three simple string operations — get, set, remove — and the SDK handles turning the session into and out of a string for you.

class KeychainStore : KeyValueStore {
    override suspend fun get(key: String): String? = /* read */
    override suspend fun set(key: String, value: String) { /* write */ }
    override suspend fun remove(key: String) { /* delete */ }
}
 
val sessionManager = createSessionManager(
    authClient = auth,
    supabaseClient = client,
    config = SessionConfig(storage = KeyValueSessionStorage(KeychainStore())),
)
 
// On launch: load the stored session and refresh it in one call.
// Note: restoreSession() always performs a token refresh (rotating the refresh
// token), so it spends one network round-trip rather than reusing a still-valid
// stored access token as-is.
sessionManager.restoreSession()

Storage is intentionally pluggable — the SDK bundles no Keychain or EncryptedSharedPreferences dependency, so you choose exactly how sessions are stored.