high priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

sessionStateStream is a broadcast Stream<SessionState> that replays the last emitted state to new subscribers (use BehaviorSubject or StreamController with seed)
A top-level Riverpod StreamProvider<SessionState> (sessionStateProvider) is defined and wires to SupabaseSessionManager.sessionStateStream
Role-based home screen widgets watch sessionStateProvider and rebuild with the correct role view when SessionState.authenticated is emitted
When SessionState.expired is emitted, all session-gated widgets show an appropriate expired session UI and route to re-authentication
When SessionState.signedOut is emitted, Riverpod providers for contacts, assignments, and activities are invalidated to clear cached data
SessionState.refreshing causes session-gated widgets to show a non-blocking loading indicator rather than blocking the UI
No explicit state management calls (e.g., ref.read(sessionProvider.notifier).update()) are needed in individual feature screens — all updates propagate automatically via the stream
Supabase Auth's own onAuthStateChange stream is bridged into sessionStateStream so external session changes (e.g., admin deactivation) are surfaced without polling
The Riverpod provider hierarchy is documented in a provider_graph.dart file with comments showing dependencies
Stream subscription is properly disposed when SupabaseSessionManager is disposed — no dangling subscriptions

Technical Requirements

frameworks
Riverpod (StreamProvider, ProviderScope, ref.watch, ref.listen)
Dart async (StreamController, broadcast streams)
apis
Supabase Auth SDK — supabase.auth.onAuthStateChange (bridge into sessionStateStream)
Riverpod — StreamProvider, KeepAliveLink
data models
assignment (invalidate cached assignments on signedOut)
contact (invalidate cached contacts on signedOut)
activity (invalidate cached activities on signedOut)
performance requirements
Session state change must propagate to all watching widgets within one frame (< 16ms) on the main isolate
Stream bridge from Supabase onAuthStateChange must not introduce more than 50ms latency
Provider invalidation on signedOut must complete synchronously within the same microtask
security requirements
sessionStateStream must not include the raw JWT token in its payloads — authenticated state carries only AppUser with role and organization fields
Riverpod providers exposing sensitive data (contact PII, assignment details) must check SessionState before returning data — return empty/error state if not authenticated
Provider tree must not cache sensitive data after signedOut invalidation — verify with memory profiler that data is GC-eligible after invalidation
ui components
SessionGateWidget — wrapper widget that listens to sessionStateProvider and renders child or expired/loading UI based on state
SessionRefreshingIndicator — subtle non-blocking overlay shown during SessionState.refreshing

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Use a BehaviorSubject (from rxdart) or implement a simple ReplaySubject with a seed value for sessionStateStream — new subscribers must receive the current state immediately to avoid race conditions during widget tree initialization. Bridge Supabase's onAuthStateChange into sessionStateStream by subscribing in SupabaseSessionManager's constructor: map AuthChangeEvent.signedIn → SessionState.authenticated, AuthChangeEvent.signedOut → SessionState.signedOut, AuthChangeEvent.tokenRefreshed → SessionState.authenticated. For Riverpod wiring, create a single sessionStateProvider as a StreamProvider.autoDispose that calls ref.keepAlive() — this ensures it stays alive throughout the app lifecycle but cleans up on test teardown. For cross-provider invalidation on sign out, add a ref.listen(sessionStateProvider, ...) in the Riverpod ProviderContainer setup (in main.dart or app_providers.dart) that calls ref.invalidate() on data providers when signedOut is observed.

Avoid adding this logic to individual feature providers to keep the invalidation centralized. Create a SessionGateWidget that uses ConsumerWidget with ref.watch(sessionStateProvider) and returns the appropriate UI for each state — this reduces boilerplate across screens.

Testing Requirements

Unit tests for stream bridging: (1) Supabase onAuthStateChange emits signedIn event — verify sessionStateStream emits SessionState.authenticated with correct AppUser. (2) Supabase onAuthStateChange emits signedOut event — verify sessionStateStream emits SessionState.signedOut. (3) New subscriber receives last emitted state immediately (BehaviorSubject replay behavior). Widget tests: (4) SessionGateWidget renders child when sessionStateProvider is authenticated.

(5) SessionGateWidget renders expired UI when sessionStateProvider is expired. (6) Role-based home screen rebuilds correctly when authenticated state changes user role. (7) Contact list provider returns empty AsyncError when sessionStateProvider is signedOut. Integration test: full sign-in → refresh → sign-out flow verified via ProviderContainer in test environment.

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.