critical priority medium complexity infrastructure pending infrastructure specialist Tier 1

Acceptance Criteria

sendPushNotification(recipientId, title, body, data) retrieves all FCM tokens for recipientId and sends a message to each via the FCM HTTP v1 API (server-side call via Supabase Edge Function)
Each send attempt is persisted to a `push_notification_log` Supabase table with: notification_id, recipient_id, fcm_token, title, body, status (pending/sent/failed), fcm_message_id, attempt_count, sent_at, error_message
sendBulkPushNotifications(recipients: List<String>, payload) fans out individual send calls with concurrency capped at 10 simultaneous requests
Retry logic: on transient FCM error (HTTP 429, 500, 503), retry up to 3 times with exponential backoff (1s, 2s, 4s base delays with jitter)
Non-retryable FCM errors (HTTP 400, 404 invalid token) immediately mark the token as invalid and trigger token revocation via FcmTokenRepository
Delivery log status is updated to 'sent' with fcm_message_id on success, 'failed' with error_message on permanent failure
sendPushNotification returns a SendResult object containing success/failure counts and the notification_id for tracking
Bulk send respects per-user multi-device: if a user has 3 devices, all 3 tokens receive the message
No PII (names, health data) is included in FCM message payloads — data field contains only IDs and notification_type

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Flutter SDK
http (Dart HTTP client for FCM HTTP v1 API calls from Edge Function)
apis
FCM HTTP v1 API (invoked via Supabase Edge Function to keep server key secret)
Supabase Edge Functions (Deno runtime)
Supabase PostgREST for delivery log writes
data models
push_notification_log (notification_id, recipient_id, fcm_token, title, body, status, fcm_message_id, attempt_count, sent_at, error_message)
fcm_device_tokens (from task-007)
certification_expiry_tracking (for notification context)
performance requirements
Single recipient send must complete (including log write) within 3 seconds under normal conditions
Bulk send of 100 recipients must complete within 30 seconds using concurrency of 10
Exponential backoff must not block the UI thread — all retries run in background isolates or async chains
security requirements
FCM server key must NEVER be stored in the Flutter app bundle — all FCM API calls go through a Supabase Edge Function
Edge Function must validate the caller is an authenticated Supabase user before dispatching FCM calls
push_notification_log must have RLS: users can only read their own log rows; writes restricted to service role (Edge Function)
FCM data payloads must not contain sensitive mentor health or personal data — use notification_type + entity_id only

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

The FCM HTTP v1 API requires OAuth2 service account credentials — these must live exclusively in the Supabase Edge Function environment variables, never in the Flutter app. The Flutter side calls the Edge Function via `supabase.functions.invoke('send-push-notification', body: {...})`. Design the Edge Function to accept a list of (token, payload) pairs so the Flutter client batches before invoking, minimizing Edge Function cold starts. For exponential backoff, use a simple `Future.delayed(Duration(seconds: delay))` chain with a random jitter of 0–500ms to prevent thundering herd on bulk sends.

When a 404 (invalid token) is returned by FCM, immediately call FcmTokenRepository.revokeToken(token) from within the retry handler — do not wait for a separate cleanup job. The `data` map in FCM messages should follow a strict schema: `{notification_type: 'certificate_expiry', threshold_days: '30', mentor_id: '...'}` — document this schema in the Edge Function README.

Testing Requirements

Write flutter_test unit tests with mocked Supabase client and mocked Edge Function HTTP calls. Test cases must cover: (1) sendPushNotification with a recipient that has 2 device tokens sends 2 FCM calls and creates 2 log rows, (2) FCM HTTP 429 response triggers retry up to 3 times then marks status as 'failed', (3) FCM HTTP 404 response triggers immediate token revocation and marks status as 'failed' without retry, (4) sendBulkPushNotifications with 15 recipients respects concurrency cap of 10 (verify via call timing or mock invocation order), (5) successful send updates log row status to 'sent' with fcm_message_id, (6) SendResult contains correct success and failure counts after a mixed-result bulk send. Write one integration test (tagged `@integration`) that calls the Supabase Edge Function in a staging environment. Minimum 80% method coverage on Flutter-side code.

Component
FCM Push Notification Sender
infrastructure medium
Epic Risks (3)
high impact medium prob technical

The RLS policy predicate that checks certification_expiry_date and suppression_status on every coordinator list query could cause full table scans at scale, degrading response time for coordinator contact list screens across all chapters.

Mitigation & Contingency

Mitigation: Add a partial index on (certification_expiry_date, suppression_status) filtered to active mentors. Benchmark the policy predicate against a representative data set (500+ mentors) during development using EXPLAIN ANALYZE on Supabase staging.

Contingency: If the index does not resolve the performance issue, introduce a computed boolean column is_publicly_visible that is updated by the mentor_visibility_suppressor service and indexed separately, shifting the predicate cost to write time rather than read time.

medium impact medium prob integration

FCM device tokens become invalid when users reinstall the app or switch devices. If the token management strategy does not handle token refresh reliably, notification delivery will silently fail for a significant portion of the user base without surfacing errors.

Mitigation & Contingency

Mitigation: Implement the FCM token refresh callback in the Flutter client to upsert the latest token to Supabase on every app launch. Store token with a last_refreshed_at timestamp. The FCM sender should handle UNREGISTERED error codes by deleting stale tokens.

Contingency: If token staleness becomes widespread, add a token health check that forces re-registration during the expiry check edge function run by querying mentors whose token was last refreshed more than 30 days ago and triggering a silent push to prompt re-registration.

medium impact low prob integration

The certification expiry and notification record tables may have column naming or constraint conflicts with existing tables in the peer mentor status and certification management features, causing migration failures in shared Supabase environments.

Mitigation & Contingency

Mitigation: Audit existing table schemas for user_roles, certifications, and notification tables before writing migrations. Prefix new columns with expiry_ to avoid collisions. Run migrations against a clean Supabase branch environment before merging.

Contingency: If a conflict is found post-merge, apply ALTER TABLE migrations to rename conflicting columns and issue a hotfix migration. Communicate schema changes to all dependent feature teams via a shared migration changelog.