critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

NotificationRepository is a Dart class with SupabaseClient injected via constructor
getNotifications(String userId, {int limit = 20, int offset = 0}) returns Future<List<AppNotification>> ordered by created_at DESC with pagination support
markAsRead(String notificationId) performs UPDATE SET is_read = true, read_at = now() WHERE id = notificationId AND user_id = auth.uid()
markAllAsRead(String userId) performs UPDATE SET is_read = true, read_at = now() WHERE user_id = userId AND is_read = false in a single query
getUnreadCount(String userId) returns Future<int> using a COUNT query WHERE user_id = userId AND is_read = false
deleteNotification(String notificationId) performs DELETE WHERE id = notificationId AND user_id = auth.uid() (RLS also enforces this)
watchUnreadCount(String userId) returns Stream<int> backed by Supabase Realtime subscription that emits the current unread count on every INSERT or UPDATE event on the notifications table for that user
AppNotification is an immutable Dart class (or freezed) with fields: id, userId, title, body, category (NotificationCategory enum), isRead, readAt (nullable), deepLink (nullable), metadata (Map<String,dynamic>), createdAt
notificationRepositoryProvider is a Riverpod Provider<NotificationRepository>
notificationsProvider(userId) is a Riverpod StateNotifierProvider or AsyncNotifierProvider supporting pagination (load more)
unreadCountProvider(userId) is a Riverpod StreamProvider<int>
All methods handle SupabaseException and rethrow as typed AppException
markAllAsRead and getUnreadCount are used together to update the unread badge on the bottom navigation tab

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Flutter SDK
apis
Supabase REST API (PostgREST)
Supabase Realtime
data models
notifications
AppNotification
NotificationCategory
performance requirements
getNotifications must use limit/offset pagination — never fetch all rows for a user
getUnreadCount must use a server-side COUNT aggregate, not client-side list length
markAllAsRead must be a single UPDATE batch, not a loop of individual updates
Realtime subscription for unread count should trigger a getUnreadCount() re-fetch rather than attempting client-side delta counting to avoid drift
security requirements
deleteNotification must include user_id = auth.uid() in the WHERE clause as defence-in-depth even though RLS enforces it
metadata field must be treated as untrusted input when rendered in UI — escape all values before display
deepLink values must be validated against an allowlist of internal routes before navigation to prevent open redirect
ui components
Bottom navigation unread badge (reads from unreadCountProvider)
Notification list item widget (renders AppNotification fields)
Notification centre screen (consumes notificationsProvider with pagination)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Place in lib/features/notifications/data/notification_repository.dart. For pagination, use the Supabase Flutter SDK .range(offset, offset + limit - 1) method. For the Realtime stream backing watchUnreadCount, subscribe to the notifications table filtered by user_id and on each event call getUnreadCount() to get a consistent server-side count — avoid incrementing/decrementing client-side counters which drift on missed events. The notificationsProvider should be an AsyncNotifierProvider with a loadMore() method that appends to the existing list, enabling infinite scroll in the notification centre UI.

AppNotification.metadata should deserialize from JSON stored as jsonb in Supabase. The deepLink field maps directly to the route path (e.g. '/activities/123') and should be validated with Uri.parse() before use. Coordinate with the bottom nav component owner to wire unreadCountProvider to the notification tab badge.

Testing Requirements

Unit tests using flutter_test with mocktail mocks for SupabaseClient. Test cases: (1) getNotifications returns correctly parsed and ordered list; (2) getNotifications with offset=20 passes correct range to query builder; (3) markAsRead calls update with correct payload and id filter; (4) markAllAsRead calls update for all unread rows in single query; (5) getUnreadCount returns integer from COUNT response; (6) deleteNotification calls delete with both id and user_id filters; (7) watchUnreadCount emits new count on Realtime INSERT event; (8) watchUnreadCount emits decremented count after markAllAsRead is called; (9) SupabaseException is rethrown as AppException with message. Integration test (optional, against local Supabase): insert 3 notifications, confirm getUnreadCount = 3, markAllAsRead, confirm getUnreadCount = 0. Minimum 80% line coverage.

Component
Notification Repository
data 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.