critical priority medium complexity infrastructure pending infrastructure specialist Tier 1

Acceptance Criteria

FCMTokenManager retrieves the FCM registration token on first call using FirebaseMessaging.instance.getToken() and returns it as a non-null String
Token refresh events via FirebaseMessaging.instance.onTokenRefresh are automatically detected and the new token is persisted to Supabase user_devices table within 5 seconds of the refresh event
Token is stored in user_devices table with correct device_id (from device_info_plus), platform ('ios' or 'android'), user_id (from Supabase auth), and updated_at timestamp
On logout, deleteToken() is called on FirebaseMessaging and the corresponding row is removed from user_devices table in Supabase
Retry logic attempts token persistence up to 3 times with exponential backoff (1s, 2s, 4s) on network failure before surfacing an error
If a token already exists for the device_id in user_devices, it is upserted (not duplicated) using device_id as the unique conflict key
Token invalidation (null token from getToken()) is detected and handled gracefully — the user_devices row is deleted and re-registration is triggered on next app launch
All Supabase operations are guarded by a check that the user is authenticated; unauthenticated calls are silently deferred until auth state changes
Unit tests cover: token fetch success, token fetch null/invalid, token refresh event, successful persist, retry on network failure, logout deletion

Technical Requirements

frameworks
Flutter
firebase_messaging
device_info_plus
Riverpod
apis
FirebaseMessaging.instance.getToken()
FirebaseMessaging.instance.onTokenRefresh
FirebaseMessaging.instance.deleteToken()
Supabase PostgREST upsert on user_devices
data models
user_devices (device_id TEXT PK, user_id UUID FK, fcm_token TEXT, platform TEXT, updated_at TIMESTAMPTZ)
performance requirements
Token persistence must complete within 3 seconds on a normal connection
Retry logic must not block the UI thread — run in an isolate-safe async context
onTokenRefresh listener must be registered once and disposed with the service lifecycle
security requirements
FCM tokens are user-scoped — never persist a token without a valid Supabase user_id
Row-Level Security (RLS) on user_devices must restrict reads/writes to the owning user only
Do not log FCM tokens in plain text in production builds

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement FCMTokenManager as a plain Dart class registered as a Riverpod Provider (not StateNotifier — it has no reactive state of its own). Inject FirebaseMessaging and SupabaseClient via constructor for testability. Use StreamSubscription for onTokenRefresh and cancel it in a dispose() method called by the owning provider's onDispose. Use device_info_plus to generate a stable device_id: on Android use androidInfo.id, on iOS use iosInfo.identifierForVendor.

For retry, implement a simple exponential backoff helper rather than pulling in a retry package. The upsert SQL should use ON CONFLICT (device_id) DO UPDATE SET fcm_token = EXCLUDED.fcm_token, updated_at = now(). Ensure this service is initialized after FirebaseApp.initializeApp() and after Supabase.initialize() in main.dart.

Testing Requirements

Write unit tests using flutter_test and mocktail. Mock FirebaseMessaging and the Supabase client. Test cases: (1) getToken returns valid string and upserts to DB, (2) getToken returns null — no DB write, (3) onTokenRefresh fires — new token upserted, (4) Supabase upsert throws SocketException — retries 3 times then emits error, (5) logout — deleteToken called and DB row removed, (6) unauthenticated state — DB call deferred. Achieve 90%+ line coverage on FCMTokenManager.

No integration tests required at this stage.

Component
FCM Token Manager
infrastructure medium
Epic Risks (4)
high impact high prob technical

Flutter's background message handler for FCM must run in a separate Dart isolate. Incorrect dependency initialization in the isolate (e.g., attempting to access Riverpod providers or Supabase before initialization) will cause silent crashes on Android when the app is terminated, resulting in missed notifications that are invisible in crash reporting.

Mitigation & Contingency

Mitigation: Use a minimal top-level background handler function annotated with @pragma('vm:entry-point') that only stores the raw RemoteMessage payload to a platform channel or shared preferences. Process the payload in the main isolate on next app launch. Write an explicit test for terminated-state message handling on Android.

Contingency: If isolate crashes are observed, implement a native Android FirebaseMessagingService subclass that handles background messages without Flutter isolate complexity, falling back to a database-insert-only approach for terminated-state notifications.

medium impact medium prob technical

Supabase Edge Functions can experience cold-start latency of 1–3 seconds after periods of inactivity. For high-frequency events like assignment creation, cumulative cold starts could cause dispatch delays exceeding the 30-second SLA, reducing the perceived reliability of the notification system.

Mitigation & Contingency

Mitigation: Configure the Edge Function with a keep-warm ping mechanism or use Supabase database webhooks that invoke the function directly on row insert to minimize cold-start frequency. Batch preference lookups within the function to reduce per-invocation Supabase round-trips.

Contingency: If latency SLA is consistently breached, move to a polling or Realtime-subscription architecture within the Edge Function, or pre-compute dispatch targets at preference-save time to eliminate per-dispatch preference queries.

high impact low prob security

If the deep link handler does not perform server-side role validation before rendering the target screen, a peer mentor who receives a mis-configured notification payload containing a coordinator-only route could access restricted data, violating the role-based access control invariants.

Mitigation & Contingency

Mitigation: The deep link handler must check the user's current role from the RoleStateManager before constructing the navigation route. Coordinator-only routes must be listed in a deny-list checked against the current role. The go_router route guard is a second line of defence.

Contingency: If a role bypass is discovered in testing, immediately add the affected route to the deep link handler deny-list and add a regression test. Audit all notification payload types for route targets that could expose cross-role data.

medium impact low prob dependency

FCM v1 HTTP API enforces per-project send quotas. For large organisations with many active peer mentors receiving simultaneous assignment notifications, batch dispatch events (e.g., bulk coordinator assignments) could approach quota limits and result in dropped notifications with 429 errors logged silently.

Mitigation & Contingency

Mitigation: Implement exponential backoff retry logic in the Edge Function for 429 responses. Design bulk assignment flows to dispatch notifications in batches with a configurable delay between batches. Monitor FCM console quotas during load testing.

Contingency: If quota limits are hit, implement a notification queue table in Supabase and a separate Edge Function that processes the queue with rate limiting, ensuring eventual delivery without exceeding FCM quotas.