critical priority medium complexity database pending database specialist Tier 0

Acceptance Criteria

Supabase table notifications exists with columns: notification_id (uuid PK default gen_random_uuid()), user_id (uuid FK → auth.users NOT NULL), category (text NOT NULL), title (text NOT NULL), body (text NOT NULL), payload (jsonb default '{}'), read_at (timestamptz nullable), created_at (timestamptz default now())
RLS SELECT policy: users can only read rows where user_id = auth.uid()
RLS UPDATE policy: users can only update read_at on their own rows (no other field updates from client)
RLS INSERT: disabled for clients — inserts are performed only by Supabase Edge Functions or service role
NotificationRepository.getNotifications(userId, {int limit, String? cursor}) returns paginated list of NotificationRecord ordered by created_at DESC
NotificationRepository.markAsRead(notificationId) sets read_at to current UTC timestamp and returns updated record
NotificationRepository.watchUnreadCount(userId) returns a Stream<int> that emits the current unread count and updates in real-time via Supabase Realtime
Unread count stream emits a new value within 500 ms of a notification being inserted server-side
Stream closes cleanly when the repository is disposed (no subscription leaks)
All methods throw typed NotificationRepositoryException on error

Technical Requirements

frameworks
Flutter
Riverpod
Supabase
apis
Supabase PostgREST (notifications table)
Supabase Realtime (notifications channel)
Supabase Auth (auth.uid())
data models
NotificationRecord
NotificationCategory (enum)
NotificationPayload
performance requirements
getNotifications paginates with limit ≤ 50 per page to avoid large payloads
watchUnreadCount uses a server-side COUNT query subscription, not client-side list counting, to minimise data transfer
markAsRead completes within 2 seconds on standard network
security requirements
INSERT is blocked for authenticated clients via RLS — only service role (Edge Function) may insert notifications
payload jsonb may contain deep-link routes but must not contain raw PII; validate structure in Edge Function before insert
RLS UPDATE restricted to read_at column only — use a separate Supabase RPC function if broader update is ever needed

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

For the real-time unread count, subscribe to the Supabase Realtime postgres_changes channel filtered by user_id and event INSERT/UPDATE on the notifications table. On each event, re-query the COUNT of rows where read_at IS NULL to get an accurate server count rather than computing locally (avoids drift). Use a StreamController.broadcast() internally in the repository so multiple listeners (e.g. bottom nav badge and notification screen) can share one subscription.

Expose the stream via a Riverpod StreamProvider for easy consumption. The payload JSONB field should be typed as Map in Dart with a fromJson factory — define a NotificationPayload class with known fields (route, entityId, entityType) and an extras map for forward compatibility. Index on (user_id, created_at DESC) and a partial index on (user_id) WHERE read_at IS NULL for efficient unread count queries.

Testing Requirements

Unit tests with mocked Supabase client: verify getNotifications constructs correct paginated query; verify markAsRead sends correct UPDATE payload; verify watchUnreadCount returns a Stream and emits updated value on simulated Realtime event. Integration tests against local Supabase: (1) user A fetches only their notifications; (2) user A cannot read user B's notifications; (3) markAsRead sets read_at and subsequent unread count stream emits decremented value; (4) stream closes without errors when subscription is cancelled. Test stream lifecycle: open stream, receive event, cancel, verify no further events. Target ≥ 90% line coverage.

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