critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

registerTokenOnSignIn(userId) calls FirebaseMessaging.instance.getToken() and receives a non-null token when permission is granted
Device fingerprint is computed deterministically from platform + device model + installation ID (no PII)
Supabase upsert to fcm_tokens uses (user_id, device_fingerprint) as the conflict target so duplicate rows are never created
Upserted row contains: user_id, fcm_token, device_fingerprint, platform (ios|android), created_at, last_refreshed_at
If FirebaseMessaging.getToken() returns null (permission not yet granted), the registration is silently deferred — no exception is thrown
registerTokenOnSignIn is invoked in the Riverpod auth state listener within 500 ms of a successful sign-in event
If Supabase upsert fails (network error), the error is caught, logged, and does not prevent the user from proceeding
Integration test confirms that after sign-in a row exists in fcm_tokens for the test user with the correct platform value
No FCM token is logged to console or Crashlytics in production builds

Technical Requirements

frameworks
Flutter
firebase_messaging ^15+
Riverpod
supabase_flutter ^2+
apis
FirebaseMessaging.instance.getToken()
Supabase fcm_tokens table upsert
flutter_secure_storage or shared_preferences for installation ID persistence
data models
fcm_tokens (user_id uuid, fcm_token text, device_fingerprint text, platform text, is_active bool, created_at timestamptz, last_refreshed_at timestamptz, revoked_at timestamptz)
performance requirements
Token registration must not block the post-sign-in navigation — run asynchronously via unawaited or a background Riverpod provider
Supabase upsert must complete within 3 seconds on a standard mobile connection
security requirements
FCM tokens must never be stored in plaintext logs or analytics events
Device fingerprint must not include IMEI, phone number, or other regulated identifiers — use flutter_device_info + package_info for safe fields only
Supabase Row Level Security (RLS) must restrict fcm_tokens writes to the authenticated user's own rows

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Place FCMTokenManager in lib/notifications/fcm_token_manager.dart as a Riverpod AsyncNotifier or plain service class. Compute device fingerprint by hashing (SHA-256) a concatenation of: Platform.operatingSystem + DeviceInfoPlugin model string + a UUID stored in FlutterSecureStorage on first install. This is stable across app updates but resets on reinstall — acceptable for token management. Listen to auth state changes via supabase.auth.onAuthStateChange stream in a Riverpod provider (ref.listen) and call registerTokenOnSignIn inside the AuthChangeEvent.signedIn branch.

Wrap the getToken() call in a try-catch because it can throw on devices where Firebase is not properly configured. Use Supabase's upsert with onConflict: 'user_id,device_fingerprint' — ensure the database has a UNIQUE constraint on those two columns before deploying.

Testing Requirements

Unit tests (flutter_test + mockito): mock FirebaseMessaging.getToken() returning a valid token, verify upsert payload structure; mock getToken() returning null, verify no exception and no Supabase call. Integration test (Supabase local/staging): sign in a test user, call registerTokenOnSignIn, query fcm_tokens and assert row exists with correct fields. Edge case test: call registerTokenOnSignIn twice for same user/device and confirm only one row exists (upsert idempotency). Security test: confirm RLS rejects an insert attempt using a different user's JWT.

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.