critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

FirebaseMessaging.instance.onTokenRefresh stream subscription is established during FCMTokenManager initialisation
On token refresh, the fcm_tokens row for (user_id, device_fingerprint) is updated with the new token and last_refreshed_at = now()
revokeTokenOnSignOut(userId, deviceFingerprint) sets is_active = false and revoked_at = now() on the correct row
Revocation is triggered in the Riverpod auth state listener on AuthChangeEvent.signedOut within 500 ms
deleteStaleTokens() deletes rows where last_refreshed_at < now() - interval '60 days' AND is_active = false
deleteStaleTokens() is callable from a scheduled Supabase Edge Function or admin endpoint — not automatically invoked on every app start
If the user signs out while offline, revocation is queued and retried on next connection (or acceptable: best-effort with a logged warning)
All three lifecycle operations are unit-tested with mocked Supabase client
Integration test confirms is_active=false after sign-out and that the stale-token cleanup removes rows older than 60 days

Technical Requirements

frameworks
Flutter
firebase_messaging ^15+
Riverpod
supabase_flutter ^2+
apis
FirebaseMessaging.instance.onTokenRefresh (StreamSubscription)
Supabase fcm_tokens table update/delete
Supabase Edge Functions (for scheduled deleteStaleTokens trigger — out of scope for this task but interface must be ready)
data models
fcm_tokens (is_active bool, revoked_at timestamptz, last_refreshed_at timestamptz)
performance requirements
onTokenRefresh handler must complete Supabase update within 5 seconds
revokeTokenOnSignOut must not delay navigation to the sign-out confirmation screen
security requirements
Revoked tokens (is_active=false) must be excluded from all push-send queries server-side
deleteStaleTokens() must only be executable by a service-role key (server-side), not the client anon key
StreamSubscription must be cancelled in the provider's dispose to prevent memory leaks

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Store the onTokenRefresh StreamSubscription as a field in FCMTokenManager and cancel it in a dispose() method called by the Riverpod provider's onDispose callback. For offline revocation, use a simple local flag in SharedPreferences ('pending_revocation': true) set before the Supabase call; on next app start in the auth listener, check the flag and retry. Avoid calling deleteStaleTokens() on the client — expose it as a Supabase Database Function with SECURITY DEFINER and call it from a pg_cron job (daily) or a Supabase Edge Function with a service-role key. This keeps the cleanup server-authoritative and prevents abuse.

Document the Supabase function signature so a backend task can implement it independently.

Testing Requirements

Unit tests (flutter_test + mockito): mock onTokenRefresh stream emitting a new token, verify Supabase update call with correct payload; mock sign-out event, verify revokeTokenOnSignOut update sets is_active=false and revoked_at. Integration tests: manually emit a token refresh on a test device, query fcm_tokens and verify last_refreshed_at updated; sign out, confirm is_active=false. Stale-token test: insert a row with last_refreshed_at 61 days ago, call deleteStaleTokens(), confirm row is deleted. Leak test: initialise and dispose the Riverpod provider, confirm StreamSubscription.cancel() is called.

Component
FCM Token Manager
infrastructure medium
Epic Risks (3)
high impact medium prob scope

iOS only allows one system permission prompt per app install. If the rationale dialog timing or content is wrong the user may permanently deny permissions during onboarding, permanently blocking push delivery for that device with no recovery path short of manual system settings navigation.

Mitigation & Contingency

Mitigation: Design and user-test the rationale dialog content and trigger point (after onboarding value-demonstration step, not at first launch). Implement the settings-deep-link fallback in NotificationPermissionManager so the permission state screen always offers a path to system settings if denied.

Contingency: If denial rates are high in TestFlight testing, revise the rationale copy and trigger timing before production release. Ensure the in-app notification centre provides full value without push so denied users are not blocked from the feature.

medium impact medium prob technical

FCM token rotation callbacks can fire at any time, including during app termination or network outage. If the token rotation is not persisted reliably the backend trigger service will dispatch to a stale token, resulting in silent notification failures that are hard to diagnose.

Mitigation & Contingency

Mitigation: Persist token rotation updates with a local queue that retries on next app foreground if network is unavailable. Use Supabase upsert by (user_id, device_id) to prevent duplicate token rows and ensure the latest token always wins.

Contingency: If token staleness is observed in production, add a token validity check on each app foreground and force a re-registration if the stored token does not match the FCM-reported current token.

high impact low prob security

Incorrect RLS policies on notification_preferences or fcm_tokens could expose one user's preferences or device tokens to another user, or could block the backend Edge Function service role from reading token lists needed for dispatch, silently dropping all notifications.

Mitigation & Contingency

Mitigation: Write explicit RLS policy tests using the Supabase test harness covering user-scoped read/write, service-role read for dispatch, and cross-user access denial. Review policies during code review with a security checklist.

Contingency: Maintain a rollback migration that reverts the RLS changes, and add an integration test in CI that asserts the service role can query all tokens and that a normal user JWT cannot access another user's token rows.