high priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

signOut() calls supabase.auth.signOut() to invalidate the session server-side before clearing local state
All tokens (access token, refresh token, biometric-gated session token) are deleted from SecureSessionStorage regardless of whether the Supabase API call succeeds or fails
Riverpod session state stream emits SessionState.signedOut(reason: SignOutReason.userInitiated) after token clearing
Active proactive refresh timer is cancelled as part of signOut() to prevent post-signout refresh attempts
Navigation is reset to the organization selection screen — the back stack is fully cleared so the user cannot navigate back to authenticated screens
Server-side revocation path: when validateCurrentSession() returns revoked, signOut() is called automatically with SignOutReason.serverRevoked — behavior is identical to user-initiated except navigation bypasses confirmation dialog
If Supabase signOut API call fails due to network error, local token clearing and navigation still proceed — best-effort server-side invalidation
Biometric credential association (if stored for re-auth shortcut) is also cleared from SecureSessionStorage on sign out
Sign out is idempotent — calling signOut() when already signed out emits SessionState.signedOut without errors
Device push notification token is deregistered from Supabase device_tokens table on successful sign out to prevent notifications to signed-out users

Technical Requirements

frameworks
Riverpod (provider invalidation / state reset)
Flutter Navigator / GoRouter (navigation stack reset)
flutter_secure_storage (token clearing)
apis
Supabase Auth SDK — supabase.auth.signOut()
Supabase database — DELETE from device_tokens where user_id = current_user
data models
device_token (deregister FCM token on sign out)
performance requirements
Token clearing from SecureSessionStorage must complete in under 500ms
Navigation to org selection screen must complete in under 200ms after state reset
Total signOut() operation from user tap to org selection screen visible: under 1.5 seconds
security requirements
Token clearing must be atomic — no window where partial tokens remain after a crash mid-signout; use a single SecureSessionStorage.clearAll() call rather than individual deletes
Ensure all Riverpod providers that cache user data (contacts, assignments, activities) are also invalidated to prevent data leakage across sessions
Back navigation after sign out must be impossible — use GoRouter.go() not pushReplacement() to ensure full stack clear
Biometric shortcut token cleared — prevents biometric re-auth from restoring a revoked session

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Implement signOut() in two phases to ensure local security even on API failure: Phase 1 (best-effort server) — try supabase.auth.signOut() in a try/catch, log any error at warning level but do not rethrow. Phase 2 (guaranteed local) — call SecureSessionStorage.clearAll(), cancel the refresh timer, invalidate all Riverpod containers via ref.invalidate() for the session provider. For navigation, inject a NavigationService abstraction rather than accessing GoRouter directly in SupabaseSessionManager — the session manager is a service layer class and should not depend on the navigation layer directly. Emit SessionState.signedOut before triggering navigation so widgets reacting to session state have a chance to clean up before they are removed from the tree.

For device token deregistration, fire-and-forget via unawaited() — it should not block the sign out flow.

Testing Requirements

Unit tests: (1) User-initiated signOut — verify supabase.auth.signOut() called, all SecureSessionStorage keys cleared, SessionState.signedOut(userInitiated) emitted, refresh timer cancelled. (2) Network failure during signOut — verify local state is still cleared and signedOut state emitted despite API error. (3) Server-revocation path — trigger validateCurrentSession returning revoked, verify automatic signOut with SignOutReason.serverRevoked. (4) Idempotency — call signOut twice, verify no duplicate Supabase API call on second invocation.

(5) Device token deregistration — verify Supabase delete called for device_token record. Widget test: verify navigation resets to org selection screen with empty back stack after signOut.

Component
Supabase Session Manager
infrastructure medium
Epic Risks (3)
high impact medium prob technical

Multiple concurrent callers (e.g., SessionResumeManager and a background sync service) could simultaneously detect a near-expired token and each invoke SupabaseSessionManager.refreshSession(), causing duplicate refresh API calls and potentially a token invalidation race condition on the Supabase Auth server. This can result in one caller receiving a valid refreshed token while another receives a 401, causing intermittent authentication failures.

Mitigation & Contingency

Mitigation: Implement a single-flight pattern inside SupabaseSessionManager so that concurrent refresh calls coalesce into one in-flight request. Use a Dart Completer or AsyncMemoizer to ensure all waiters receive the same refreshed token. Write a concurrent integration test to validate the single-flight behaviour.

Contingency: If the single-flight pattern introduces deadlocks or timeout complexity, fall back to a mutex-based lock with a 10-second timeout, logging a warning if the lock is held longer than expected, and triggering a full re-login if the refresh ultimately fails.

high impact low prob security

Supabase Row-Level Security policies evaluate the JWT claims (user_id, role, org_id) on every query. If the refreshed token contains stale or changed claims — for example if a coordinator's role was updated server-side — RLS may silently block data access even though the session appears valid from the client's perspective, causing confusing empty screens rather than an authentication error.

Mitigation & Contingency

Mitigation: After every token refresh, decode the new JWT and compare key claims (role, org_id) with the cached values. If claims have changed, emit a session-claims-changed event that triggers a role re-resolution and navigation reset. Document this behaviour in the SupabaseSessionManager API contract.

Contingency: If claims drift is detected in production and causes data visibility issues, provide a force-refresh mechanism in the UI (pull-to-refresh on home screen) that clears cached role state and re-fetches from Supabase, accompanied by a user-visible toast indicating the session was refreshed.

medium impact medium prob security

Allowing session resumption from cached local token when offline introduces a window where a revoked or invalidated session can still grant app access. For example, if a coordinator deactivates a peer mentor's account while the mentor is offline, the mentor continues to have access until connectivity is restored and the token is validated server-side.

Mitigation & Contingency

Mitigation: Set a maximum offline grace period (e.g., 24 hours) stored alongside the token in SecureSessionStorage. If the grace period is exceeded, force a full credential re-login regardless of connectivity status. Scope offline access to read-only operations only, requiring connectivity for any write that reaches Supabase.

Contingency: If the offline grace period logic is found to be insufficient for compliance, implement remote session invalidation via a lightweight push notification that clears SecureSessionStorage even when the app is backgrounded, using FCM with a data-only message.