critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

A single dispatcher.dispatch(payload, recipientUserId) call triggers both FCM push dispatch (task-006) and in-app record insertion in a coordinated sequence
In-app notification record is inserted into the Supabase notifications table with fields: id (UUID), user_id, title, body, notification_type ('pause_status'), read (false), created_at (server timestamp), metadata (JSON with pause event context)
If FCM push dispatch succeeds and Supabase insert fails, the overall DispatchResult reflects a partial success (push_ok_inapp_failed) — the push is NOT rolled back
If FCM push dispatch fails, the Supabase in-app record is still inserted so the user sees the notification in-app regardless of push delivery
If both FCM and Supabase fail, DispatchResult is dead_letter
Supabase write uses the service-role key (server-side only) so RLS does not block the insert
Inserted record is immediately visible to authenticated queries from the recipient user (respects RLS read policy)
The in-app channel does not throw or propagate exceptions to callers — all errors are captured into the DispatchResult
Duplicate notification prevention: if a notification for the same user_id + event_id already exists in the table, skip insert and return existing record id

Technical Requirements

frameworks
Flutter
Dart
Supabase
Riverpod
apis
Supabase REST API (PostgREST) — notifications table insert
Supabase Edge Functions (server-side execution context)
data models
NotificationRecord
DispatchResult (extended with inAppStatus)
PauseNotificationPayload
performance requirements
Supabase insert must complete within 3 seconds
In-app insert must not add latency to FCM push path — use Future.wait or concurrent execution where safe
security requirements
Insert must execute with service-role credentials server-side; anon key must never be used for server-initiated writes
RLS read policy on notifications table must restrict reads to owner (user_id = auth.uid())
notification metadata field must not contain PII beyond what is required for display (no full names, no addresses)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Model the dual-channel dispatch as a coordinator function (not a class method on FCM dispatcher) that calls the FCM channel and the in-app channel concurrently using Future.wait where possible, then merges the two outcomes into a unified DispatchResult. The key design principle: the two channels are independent — a failure in one must not prevent the other from running. Use try/catch around each channel invocation independently. For the Supabase insert, use upsert with onConflict: 'user_id,event_id' to implement idempotency.

Define a NotificationRecord Dart model with fromJson/toJson and map it to the Supabase row. Keep the in-app channel as a separate injectable class so it can be mocked independently in tests. The coordinator function is what the BLoC event handler calls — it abstracts channel details from business logic.

Testing Requirements

Unit tests using flutter_test with mocked Supabase client. Required test cases: (1) both channels succeed — result is full_success with both push message_id and in-app record id, (2) FCM succeeds, Supabase fails — result is push_ok_inapp_failed, record not present in mock store, (3) FCM fails, Supabase succeeds — result is push_failed_inapp_ok, in-app record is present, (4) both fail — result is dead_letter, (5) duplicate event_id — second call skips insert and returns existing record id. Integration test against a Supabase test project: verify RLS allows recipient to read inserted record and blocks other users.

Component
FCM Notification Dispatcher
infrastructure medium
Epic Risks (3)
high impact medium prob integration

The org membership table structure used to resolve coordinator relationships may differ from what the repository assumes, causing incorrect coordinator lookup or missing rows for mentors in multi-chapter scenarios.

Mitigation & Contingency

Mitigation: Review the existing org membership table schema and RLS policies before writing repository queries. Align query logic with the patterns already used by peer-mentor-status-repository and multi-chapter-membership-service.

Contingency: If schema differs, add an adapter layer in the repository that normalises the membership resolution and document the discrepancy for the data team. Fall back to coordinator lookup via the feature's own stored coordinator_id field if org membership join fails.

high impact medium prob technical

Device tokens stored in the database may be stale or unregistered, causing FCM dispatch failures that silently drop coordinator notifications — the primary coordination safeguard of this feature.

Mitigation & Contingency

Mitigation: Implement token validation on every dispatch call and handle FCM's NOT_REGISTERED error by flagging the token as invalid in the database. Reuse the token refresh pattern already established by fcm-token-manager.

Contingency: If push delivery fails after retry, ensure the in-app notification record is always written regardless of push outcome so coordinators can still see the event in the notification centre.

medium impact low prob technical

The optional reason field may contain special characters, emoji, or non-Latin scripts that exceed the 200-character byte limit when FCM encodes the payload, causing delivery failures.

Mitigation & Contingency

Mitigation: Enforce the 200-character limit on Unicode code point count, not byte count, in the payload builder. Add a unit test with multi-byte input strings.

Contingency: If an oversized payload is detected at dispatch time, strip the reason field from the push notification body and note 'See in-app notification for full reason' to preserve delivery.