high priority medium complexity infrastructure pending infrastructure specialist Tier 0

Acceptance Criteria

FCMTokenManager successfully registers a device token on first app launch and the token is persisted to the authenticated user's Supabase profile row
When Firebase issues a new token (onTokenRefresh callback), the manager detects the change, replaces the stored token in Supabase, and emits the updated value on the active token stream
On app reinstall, the old token is invalidated client-side and a fresh token is registered and stored without requiring user action
If the Supabase upsert fails (network error, auth expiry), the manager retries with exponential back-off (3 attempts, 2s/4s/8s) and logs the failure
The activeToken stream emits null when no token is available (e.g., permission not yet granted) and a non-null String when a valid token exists
Multiple downstream subscribers to the stream receive the same current token without triggering duplicate Supabase writes
FCMTokenManager is registered as a singleton in the DI container (Riverpod provider) and initialised before any notification-dependent service
Unit tests verify token refresh logic, Supabase persistence call, stream emission, and retry behaviour using mocked Firebase and Supabase clients
No token value is written to device logs or crash reports in production builds

Technical Requirements

frameworks
Flutter
Riverpod
firebase_messaging (flutter_fire)
apis
FirebaseMessaging.instance.getToken()
FirebaseMessaging.instance.onTokenRefresh stream
Supabase PostgREST upsert on user_fcm_tokens or user_profiles table
data models
UserProfile (fcm_token field)
FCMToken (device_id, token, updated_at)
performance requirements
Token registration must complete within 3 seconds on a standard 4G connection
Stream subscription must not allocate more than one active FirebaseMessaging listener per app lifecycle
Supabase upsert must be debounced to avoid duplicate writes on rapid token refresh events
security requirements
FCM token must only be stored against the authenticated user's row — enforce RLS policy on Supabase
Token must be cleared from Supabase on sign-out to prevent stale push delivery to previous session
Never log raw token values at info/debug level in production builds; use obfuscated identifiers
Use Supabase service role only server-side; client uses anon key with RLS

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use a Riverpod AsyncNotifier or StreamProvider to expose the active token. Initialise by calling FirebaseMessaging.instance.getToken() inside the provider's build method, then merge with the onTokenRefresh stream using StreamController or rx_dart's BehaviorSubject to replay the last value to new subscribers. Persist to Supabase using an upsert on a user_fcm_tokens table keyed by (user_id, platform) to support multi-device. Call FirebaseMessaging.instance.deleteToken() and null out the Supabase row in the sign-out hook.

Register the provider initialisation in main.dart after Supabase.initialize() and FirebaseApp.initialize(), before runApp(). Avoid storing the token in SharedPreferences as a primary source of truth — Supabase is authoritative; local cache is only for offline resilience.

Testing Requirements

Unit tests (flutter_test + mockito/mocktail): mock FirebaseMessaging to simulate getToken success/failure and onTokenRefresh events; mock Supabase client to assert upsert is called with correct payload; verify stream emits correct sequence (null → token → refreshed token). Integration test: verify FCMTokenManager initialises correctly in a widget test environment using a fake Supabase instance. Edge cases: test null token return from Firebase (permissions denied), Supabase 401 during upsert, rapid consecutive refresh events. Target 90%+ line coverage on FCMTokenManager class.

Component
Notification Badge Widget
ui low
Epic Risks (2)
medium impact medium prob technical

The notification badge widget depends on a persistent Supabase Realtime websocket subscription for live unread count updates. On mobile, network transitions (WiFi to cellular, background app state) can silently drop the websocket, resulting in a stale badge count that does not update until the next app foreground — reducing trust in the notification system.

Mitigation & Contingency

Mitigation: Implement connection lifecycle management in the badge widget's BLoC that re-subscribes on app foreground and on network reconnection events. Add a fallback polling query (every 60 seconds when app is foregrounded) to reconcile the badge count if the Realtime subscription is interrupted.

Contingency: If Realtime reliability proves insufficient in production, replace the live subscription with a polling approach using a configurable interval, accepting slightly delayed badge updates in exchange for reliability.

medium impact medium prob technical

The notification list item widget requires merged semantics combining title, body, timestamp, read state, and role-context icon into a single VoiceOver/TalkBack announcement. Getting the merged semantics structure right for both iOS (VoiceOver) and Android (TalkBack) simultaneously is non-trivial and common to break silently when widgets are refactored.

Mitigation & Contingency

Mitigation: Use the project's existing semantics-wrapper-widget pattern with explicit Semantics widgets and excludeSemantics on decorative children. Write accessibility widget tests using Flutter's SemanticsController to assert the exact announcement string. Test on physical devices with VoiceOver and TalkBack enabled before release.

Contingency: If merged semantics cannot be achieved cleanly on both platforms, implement platform-specific semantic trees using defaultTargetPlatform branching, ensuring each platform receives an optimal announcement even if the implementation differs.