critical priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

NotificationPreferencesRepository is a Dart class with a single SupabaseClient dependency injected via constructor
getPreferences(String userId) returns Future<List<NotificationPreference>> fetching all rows for the user ordered by category
updatePreference(String userId, NotificationCategory category, bool enabled) performs an UPSERT on (user_id, org_id, category) and updates enabled + updated_at
initializeDefaultPreferences(String userId, String orgId) inserts one row per NotificationCategory with enabled=true using INSERT ... ON CONFLICT DO NOTHING so it is safe to call multiple times
watchPreferences(String userId) returns Stream<List<NotificationPreference>> backed by Supabase Realtime subscription on notification_preferences WHERE user_id = userId
Stream emits the full updated list on every INSERT/UPDATE event, not just the delta
NotificationCategory is a Dart enum with values: activityReminder, certificationExpiry, pauseStatus, scenarioPrompt, system; serialization to/from snake_case strings handled
NotificationPreference is an immutable Dart class (or freezed) with fields matching the table schema
notificationPreferencesRepositoryProvider is a Riverpod Provider<NotificationPreferencesRepository> available app-wide
notificationPreferencesProvider(userId) is a Riverpod StreamProvider<List<NotificationPreference>> that exposes the real-time stream
All async methods handle SupabaseException and rethrow as typed AppException with a meaningful message
Unit tests pass for all public methods using a mocked SupabaseClient

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Flutter SDK
apis
Supabase REST API (PostgREST)
Supabase Realtime
data models
notification_preferences
NotificationPreference
NotificationCategory
performance requirements
initializeDefaultPreferences must complete in a single round-trip using bulk INSERT with ON CONFLICT DO NOTHING, not 5 sequential inserts
Realtime subscription must be disposed when the provider is disposed to prevent memory leaks
security requirements
Repository must use the authenticated SupabaseClient (not service role) — RLS enforces access control
userId parameter must match auth.currentUser.id; the repository should assert this in debug mode
No raw SQL strings in Dart code — use the Supabase Flutter SDK query builder exclusively

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Place the repository in lib/features/notifications/data/notification_preferences_repository.dart following the project's feature-based directory structure. Define NotificationPreference as a freezed data class for immutability and copyWith support. The Realtime stream should use supabase.from('notification_preferences').stream(primaryKey: ['id']).eq('user_id', userId) which returns a Stream>> — map each event to a full getPreferences() call or parse inline. Prefer the .stream() approach over .on() for type safety.

For the Riverpod provider, use a family provider pattern: final notificationPreferencesProvider = StreamProvider.family, String>((ref, userId) => ref.watch(notificationPreferencesRepositoryProvider).watchPreferences(userId)). Dispose the Realtime channel in the ref.onDispose callback to prevent socket leaks.

Testing Requirements

Unit tests using flutter_test with a mocked SupabaseClient (use mockito or mocktail). Test cases: (1) getPreferences returns parsed List from mock response; (2) updatePreference calls upsert with correct payload including updated_at; (3) initializeDefaultPreferences calls insert with 5 rows and ON CONFLICT DO NOTHING; (4) initializeDefaultPreferences called twice does not insert duplicates (mock returns empty on second call); (5) watchPreferences emits updated list on Realtime INSERT event; (6) watchPreferences emits updated list on Realtime UPDATE event; (7) getPreferences propagates SupabaseException as AppException. Minimum 80% line coverage on the repository class.

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.