high priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

NotificationTriggerService queries notification_preferences for the recipient before each FCM dispatch
If the user's preference for the given category is set to false or the record does not exist, the FCM send is skipped
Skipped dispatches are logged with: user_id (hashed or partial), notification_category, and reason='opted_out'
Skipping a user does not throw an exception or fail the trigger invocation — execution continues to the next recipient
For coordinator batch broadcasts, a single bulk query retrieves opted-in user IDs to avoid N+1 preference queries
The default behavior when no preference record exists for a user+category pair is to SEND (opt-in by default)
Preference filtering operates on the backend (trigger service) — FCM tokens are never fetched for opted-out users
Integration test confirms that a user who opts out of 'new_assignment' does not receive the FCM dispatch after the preference is saved

Technical Requirements

frameworks
Supabase Dart client
BLoC
apis
Supabase REST API (notification_preferences table)
Firebase Cloud Messaging HTTP v1 API
data models
NotificationPreference
NotificationCategory
FCMToken
performance requirements
Batch coordinator broadcasts must use a single IN query on user_ids against notification_preferences — not one query per user
Preference query result must be cached for the duration of a single trigger invocation to prevent repeated reads for the same user
Bulk preference query must return results within 300 ms for up to 200 recipients
security requirements
Preference query must be executed with the service role key — never expose preference data to client-side code
Logged skipped dispatches must not include personally identifiable information — use user_id UUID only

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Design a PreferenceFilter class with a single method filterRecipients(List userIds, String category) -> Future> that returns the filtered list of user IDs to dispatch to. This keeps filtering logic decoupled from FCM dispatch logic and independently testable. For the bulk query, use Supabase's .select('user_id').eq('category', category).eq('enabled', false).in_('user_id', userIds) then subtract the returned IDs from the full list (opt-out list approach) to preserve opt-in default for users without records. Use Dart's Set difference for the subtraction: Set optedOut = result.toSet(); return userIds.where((id) => !optedOut.contains(id)).toList().

Log skipped count (not individual user IDs) at info level and full user_id list at debug level only.

Testing Requirements

Unit tests (flutter_test / Dart test) for preference filtering logic: user with opted-out category is excluded from dispatch; user with no preference record is included (opt-in default); user with opted-in category is included; batch of 10 users with mixed preferences correctly partitions into send/skip lists. Integration test with a Supabase test project: update a user's preference to false, trigger a dispatch, assert no FCM send occurred for that user and the skip was logged. Assert that the trigger invocation still succeeds (returns success) even when all users are opted out. Performance test with 100 recipients to validate single bulk query path.

Component
Notification Trigger Service
service high
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.