critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

SupabaseSessionManager implements proactive refresh that triggers when the access token has less than refreshWindowMinutes (default: 5) remaining before expiry
Refresh window is configurable via constructor injection with a sane default of Duration(minutes: 5)
Successful refresh updates the stored session token in SecureSessionStorage and emits SessionState.authenticated with updated AppUser
On refresh network failure, exponential backoff retries with base delay 2s, multiplier 2x, capped at 60s, maximum 5 attempts before emitting SessionState.expired
On refresh auth error (401/403 from Supabase), immediately emits SessionState.expired without retrying — token is invalidated
Proactive refresh timer is cancelled and restarted correctly on successful refresh using the new token's expiry time
No duplicate refresh requests are in flight simultaneously — idempotent guard ensures only one refresh is active at a time
App lifecycle changes (AppLifecycleState.resumed) trigger an immediate expiry check — if within refresh window, refresh is attempted
Refresh mechanism works correctly when the device has been offline — on connectivity restoration it checks and refreshes if needed
All refresh activity is logged at debug level with token expiry timestamps (no token values logged)

Technical Requirements

frameworks
Riverpod (session state emission)
flutter_local_auth (for step-up re-auth trigger on refresh failure)
Dart async (Timer, StreamController)
apis
Supabase Auth SDK — supabase.auth.refreshSession()
Supabase Auth SDK — supabase.auth.onAuthStateChange stream
performance requirements
Refresh timer polling overhead must not exceed 1ms CPU per check cycle
Token expiry parsing must complete in under 1ms — parse JWT exp claim directly without full decode
No unnecessary wake-locks — timer must respect Flutter's reduce-power background modes
security requirements
Access token and refresh token values must never appear in log output — log only expiry timestamps
Refresh token is stored exclusively in flutter_secure_storage (iOS Keychain / Android Keystore) — never in SharedPreferences
On refresh auth error, clear all cached tokens immediately to prevent use of stale credentials
Refresh requests transmitted over TLS only — Supabase SDK enforces this, but confirm in integration test

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Parse JWT expiry without a full JWT library — use base64Url decode on the payload segment and extract the 'exp' Unix timestamp field directly. This avoids adding a dependency for a trivial operation. Use a single periodic Timer set to check every 60 seconds rather than scheduling a one-shot timer at exact expiry to avoid timer drift issues on Android. For the idempotent guard, use a bool _refreshInProgress flag protected by a Completer?

_pendingRefresh — if a refresh is in progress, return the same Completer's future to any concurrent caller. For exponential backoff, implement a simple RetryPolicy value class with attempt count and next delay calculation — inject it for testability. Register an WidgetsBindingObserver to detect AppLifecycleState.resumed and trigger the expiry check. Ensure the observer is properly unregistered in dispose() to prevent memory leaks.

Testing Requirements

Unit tests required using fake_async to control timer advancement without real delays. Test scenarios: (1) Token expiring in 6 minutes — verify no refresh triggered immediately. (2) Token expiring in 4 minutes — verify refresh triggered within one timer cycle. (3) Successful refresh — verify new timer scheduled based on new expiry.

(4) Network failure — verify retry sequence with correct backoff delays (2s, 4s, 8s, 16s, 32s then expired). (5) Auth error on refresh — verify immediate SessionState.expired emission with no retries. (6) App resume with token in refresh window — verify immediate refresh attempt. (7) Concurrent refresh guard — start two simultaneous refresh calls, verify only one Supabase API call is made.

Coverage target: 90%+ on refresh logic methods.

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.