critical priority low complexity database pending database specialist Tier 0

Acceptance Criteria

Supabase migration file creates a notification_preferences table with columns: id (uuid PK), user_id (uuid FK → auth.users), global_push_opt_out (boolean, default false), scenario_type_opt_outs (jsonb, default '{}'), created_at (timestamptz), updated_at (timestamptz)
RLS is enabled on notification_preferences; SELECT policy allows only rows where user_id = auth.uid(); INSERT and UPDATE policies enforce the same constraint
No DELETE policy exists — preferences are soft-managed via opt-out flags, not deleted
A unique constraint exists on user_id so each user has exactly one preferences row
NotificationPreferencesRepository interface defines: getPreferences(userId), setGlobalOptOut(userId, value), setScenarioTypeOptOut(userId, scenarioType, value), upsertPreferences(userId, prefs)
SupabaseNotificationPreferencesRepository implements the interface using supabase.from('notification_preferences').upsert() with onConflict: 'user_id'
Calling getPreferences for a user with no row returns a default NotificationPreferences object (all opt-outs false) without inserting a row
Calling setGlobalOptOut(userId, true) followed by getPreferences returns global_push_opt_out == true
All CRUD operations complete in under 500ms on a standard Supabase free-tier project
Repository methods throw typed exceptions (NotificationPreferencesException) on Supabase errors, not raw PostgrestException

Technical Requirements

frameworks
Flutter
Supabase Flutter SDK
Riverpod (for repository provider)
apis
Supabase REST API
Supabase Auth (auth.uid() for RLS)
data models
NotificationPreferences
ScenarioTypeOptOut
performance requirements
getPreferences must complete in under 500ms including network round-trip
upsertPreferences must use a single Supabase upsert call — not separate insert + update
security requirements
RLS SELECT policy: user_id = auth.uid() — no user can read another user's preferences
RLS INSERT policy: user_id = auth.uid() — users can only insert their own row
RLS UPDATE policy: user_id = auth.uid() — users can only update their own row
No service-role bypass in client-side code — all operations use the anon key with RLS
scenario_type_opt_outs JSONB keys must be validated against an enum of allowed scenario types before write to prevent arbitrary key injection

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Define a Dart abstract class NotificationPreferencesRepository with the four methods listed in acceptance criteria. Create NotificationPreferences as an immutable data class with copyWith, fromJson, and toJson. Use Supabase's upsert with onConflict: 'user_id' for both create and update operations — this ensures idempotency and avoids race conditions on first-time preference creation. The JSONB column scenario_type_opt_outs should map to a Map in Dart — define ScenarioType as a Dart enum and serialize to/from string keys in JSON (e.g., 'follow_up_activity', 'wellbeing_check').

For the default-return behavior, check if the Supabase response list is empty and return NotificationPreferences.defaults() without an extra DB call. Register the repository as a Riverpod provider: final notificationPrefsRepoProvider = Provider((ref) => SupabaseNotificationPreferencesRepository(ref.read(supabaseClientProvider))). Create the migration file in supabase/migrations/ with a timestamped filename following the project's existing migration naming convention.

Testing Requirements

Three levels of testing: (1) Unit tests — mock the Supabase client and verify that getPreferences returns a default object when the mock returns null/empty, and that upsertPreferences passes the correct payload including onConflict parameter. (2) Integration tests — against the Supabase test project: insert a preferences row, read it back, update global_push_opt_out, verify RLS blocks cross-user read (authenticate as user A and attempt to read user B's row, expect empty result). (3) RLS policy test — dedicated test that authenticates as two different test users and asserts isolation. Run unit tests with flutter test, integration tests with integration_test package.

Coverage target: >= 90% for the repository implementation class.

Component
Notification Preference Service
service low
Epic Risks (3)
high impact medium prob technical

The scenario-edge-function-scheduler must evaluate all active peer mentors within the 30-second Supabase Edge Function timeout. For large organisations, a sequential evaluation loop may exceed this limit, causing partial runs and missed notifications.

Mitigation & Contingency

Mitigation: Design the trigger engine to batch mentor evaluations using database-side SQL queries (bulk inactivity check via a single query rather than per-mentor calls), and add a performance test against 500 mentors during development. Document the evaluated mentor count per scenario type in scenario-evaluation-config to allow selective scenario execution per run.

Contingency: If single-run execution is insufficient, split evaluation into per-scenario-type scheduled functions (inactivity check, milestone check, expiry check) on separate cron schedules, dividing the computational load across multiple invocations.

high impact low prob technical

A race condition between concurrent scheduler invocations or retried cron triggers could cause the same scenario notification to be dispatched multiple times to a mentor, severely degrading trust in the feature.

Mitigation & Contingency

Mitigation: Implement cooldown enforcement using a database-level upsert with a unique constraint on (user_id, scenario_type, cooldown_window_start) so that a second invocation within the same window is rejected at the persistence layer rather than the application layer.

Contingency: Add an idempotency key derived from (user_id, scenario_type, evaluation_date) to the notification record insert; if a duplicate key violation is caught, log it as a warning and skip dispatch without error.

medium impact medium prob integration

The trigger engine queries peer mentor activity history across potentially multiple organisations and chapters. RLS policies configured for app-user roles may block the Edge Function's service-role queries, or query performance may degrade on large activity tables.

Mitigation & Contingency

Mitigation: Confirm the Edge Function runs with the Supabase service role key (bypassing RLS) and add composite indexes on (user_id, activity_date) to the activity tables before implementing the inactivity detection query.

Contingency: If service-role access is restricted by organisational policy, implement a dedicated database function (SECURITY DEFINER) that performs the inactivity aggregation and is callable by the Edge Function with limited scope.