Skip to content
Documentation GitHub
Data Flow

Authentication

How sign-in, session persistence, token refresh, and sign-out flow through the auth system.



Code: crates/infrastructure/supabase/src/auth/supabase_auth.rs

Supabase Auth supports multiple sign-in methods: OAuth (GitHub, Google), magic link (email), and email+password. The Tauri command orchestrates the flow based on the method. After a successful auth exchange, the command receives access_token and refresh_token from Supabase.

store_session is called with both tokens:

  1. Keychain persistence (best-effort): keychain_set attempts to store each token in the OS keychain under service com.inklings.desktop. Keychain failures are logged as error! (not warn!) to make session persistence failures visible, but the operation continues — tokens are still cached in memory.
  2. In-memory cache: session.write() updates the RwLock<SessionStore> with Arc<String> handles to both tokens. Subsequent get_session calls read from this lock-protected in-memory store without touching the keychain.

After the session is stored, IdentityStore::store_identity persists the AuthUser (id, email, display_name) as a JSON string under a separate keychain key. The identity survives across sessions independently of the token lifecycle.

Code: SupabaseAuthRepository::new

On construction (called during AppState initialization), the repository immediately attempts to restore tokens from the keychain:

let access_token = keychain_get(KEYCHAIN_ACCESS_TOKEN).map(Arc::new);
let refresh_token = keychain_get(KEYCHAIN_REFRESH_TOKEN).map(Arc::new);

If tokens are found, they are pre-loaded into the in-memory SessionStore. This means the first call to ensure_valid_token after startup does not need to prompt the user to sign in, as long as the stored token has not expired (or can be refreshed).

Code: supabase_auth.rsensure_valid_token, refresh_session, is_jwt_expired

ensure_valid_token is called by any command that needs to make an authenticated request to Supabase.

Expiry check: is_jwt_expired decodes the JWT payload (base64url) and reads the exp claim. It does NOT verify the signature — the Supabase API validates the signature server-side. A 30-second clock skew buffer is applied: tokens expiring within 30 seconds are treated as already expired.

Refresh serialization: A dedicated tokio::sync::Mutex<()> (refresh_lock) serializes concurrent refresh attempts. If two async tasks both see an expired token and call refresh_session concurrently:

  • The first acquires the lock and performs the refresh
  • The second blocks on lock().await until the first completes
  • When the second proceeds, it typically finds a now-valid token (though the current implementation still performs a refresh for the second caller — a future optimization could re-check expiry inside the lock)

Exponential backoff: refresh_failure_count (atomic) and last_refresh_failure track repeated refresh failures. The backoff formula is 2^min(failure_count, 5) seconds, capping at 32 seconds. check_refresh_backoff is called before acquiring refresh_lock and returns an AuthFailed error if within the backoff window. This prevents thundering herd from many concurrent callers hammering a broken refresh endpoint.

After a successful refresh, reset_refresh_backoff clears both the counter and the last failure timestamp.

Code: supabase_auth.rssign_out

Sign-out is intentionally tolerant of failures:

  1. Remote logout (best-effort): POST /auth/v1/logout is attempted with the current access token. The response is silently discarded — if the request fails (network error, token already invalid), sign-out still proceeds.
  2. Session clear: clear_session deletes both keychain entries and clears the in-memory SessionStore. This is synchronous and always succeeds regardless of remote logout outcome.
  3. Identity clear: clear_identity removes the user identity from the keychain.

After sign-out, any subsequent call to ensure_valid_token returns AuthError::NotAuthenticated.

The system enforces a conceptual separation between two concerns:

ConcernTypeLifetimeStorage
Who is the userIdentityStore / AuthUserPermanent (survives sign-out option)OS keychain, separate key
Do we have a valid tokenAuthRepository / sessionSession (cleared on sign-out)OS keychain + in-memory cache

This separation means the app can display the user’s name and identity locally without making any network calls, even when the auth session is expired. The identity is re-stored on each successful sign-in but cleared only when the user explicitly clears it.

6. In-Memory Repository (Tests / HTTP Bridge)

Section titled “6. In-Memory Repository (Tests / HTTP Bridge)”

Code: SupabaseAuthRepository::new_in_memory

A non-keychain variant is used in tests and the HTTP bridge to avoid blocking on OS keychain authorization dialogs. Identity is stored in RwLock<Option<AuthUser>> instead of the keychain. Token management works identically except keychain calls are skipped.


FailureBehavior
Keychain write fails on sign-inerror! logged; tokens still cached in memory; session will not survive restart
Keychain read fails on startupwarn! logged; session starts empty (user must sign in again)
JWT expired, refresh failsAuthError::AuthFailed returned; exponential backoff activated
Refresh rate-limited (backoff active)AuthError::AuthFailed("Token refresh rate-limited...") returned immediately
Remote logout fails (sign-out)Silently discarded; local session cleared regardless
No session (unauthenticated command)AuthError::NotAuthenticated returned to command layer
Lock poisoned (RwLock)AuthError::Storage("Lock poisoned: ...") returned
Network error (refresh)AuthError::Network { message, details } returned; failure recorded for backoff

  • Auth System — Full auth system reference including OAuth flows, device management, and Supabase Auth configuration
  • Sync System — Sync engine calls ensure_valid_token before every push/pull cycle
  • MCP System — MCP bearer token is separate from auth session; managed independently

Was this page helpful?