critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

Dispatcher constructs a valid FCM-compatible payload for a `summary-ready` event with correct `notification` title/body and a `data` map containing `deep_link`, `summary_id`, and `org_id` fields
Dispatcher constructs a valid APNs-compatible payload for the same event
Deep-link routing supports at minimum the `/summary/{summaryId}` route, navigating the user directly to their period summary screen
Dispatcher accepts a list of target `userId`s and resolves their FCM/APNs tokens from the Supabase `device_tokens` table before dispatching
Delivery confirmation is tracked: for each dispatch attempt, a record is written to a `notification_dispatch_log` table with `user_id`, `notification_type`, `dispatched_at`, `platform`, and `status` (sent/failed)
Failed deliveries (invalid token, network error) are logged with error detail but do not throw — the dispatcher continues dispatching to remaining recipients
Dispatcher calls the OrgIsolationGuard before resolving device tokens to ensure only users of the specified organisation are targeted
Notifications are not sent to users who have disabled push notifications in their app settings (check `notification_preferences` table)
Dispatcher is invocable from Supabase Edge Functions (no Flutter runtime dependency in its core logic)
Personalised notification body includes the peer mentor's first name and the period label (e.g., 'Your H1 2025 summary is ready, Anna!')

Technical Requirements

frameworks
Dart
Supabase Dart SDK
Supabase Edge Functions (for server-side dispatch)
apis
Firebase Cloud Messaging (FCM) HTTP v1 API
Apple Push Notification service (APNs)
Supabase REST API (device token and preference lookup)
data models
DeviceToken
NotificationPreference
NotificationDispatchLog
PeerMentor
SummaryPeriod
performance requirements
Dispatch to 100 recipients completes within 10 seconds
Use batch FCM send API where available to reduce HTTP round-trips
Token resolution uses a single Supabase query with an `IN` filter, not N individual queries
security requirements
FCM server key and APNs credentials are stored as Supabase Edge Function secrets — never in the Flutter app bundle
Device tokens are treated as sensitive data — never logged in plaintext
OrgIsolationGuard must be called before any token resolution to prevent cross-org notification targeting
Notification payloads must not include sensitive personal data beyond first name and summary reference

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Separate the dispatcher into two layers: (1) a `NotificationPayloadBuilder` that constructs platform-specific payloads from a `SummaryNotificationEvent` value object — this is purely functional and easily unit-tested; (2) a `PushNotificationDispatcher` that resolves tokens, calls the payload builder, sends via HTTP, and logs results. The actual HTTP dispatch to FCM/APNs should be implemented as a Supabase Edge Function (Deno/TypeScript) invoked from Dart via `supabase.functions.invoke()` — this keeps API keys server-side and avoids app bundle security risks. The Dart-side dispatcher is then responsible for: resolving tokens, filtering by preferences, constructing the event payload, and calling the edge function. Deep-link format should follow Flutter's `go_router` route naming convention already established in the app (e.g., `/summary/:id`).

Personalisation tokens (first name) must be fetched from the database, not passed by the caller, to prevent spoofing.

Testing Requirements

Unit tests with flutter_test using mock HTTP clients: verify FCM payload structure matches FCM HTTP v1 spec; verify APNs payload structure; verify deep-link value is correctly constructed for a given summaryId; verify that users with disabled notifications are excluded; verify that a failed send for one user does not abort the batch. Integration tests: use a Supabase test project with seeded device_tokens and notification_preferences; verify dispatch_log records are created with correct status. Manual acceptance test via TestFlight: trigger a summary-ready event and verify the notification arrives on an iOS device with correct title, body, and deep-link navigation.

Component
Push Notification Dispatcher
infrastructure medium
Epic Risks (4)
high impact medium prob technical

Supabase pg_cron or Edge Function retries could trigger multiple concurrent generation runs for the same period and organisation, producing duplicate summaries and sending multiple push notifications to users — a serious UX regression.

Mitigation & Contingency

Mitigation: Implement a database-level run-lock using an INSERT … ON CONFLICT DO NOTHING pattern keyed on (organisation_id, period_type, period_start). Only the first successful insert proceeds; subsequent attempts read the existing lock and exit early. Test with concurrent invocations in a Deno test suite.

Contingency: If duplicate summaries are detected post-deployment, add a deduplication cleanup job that removes all but the most recent summary per (user_id, period_type, period_start) and sends a corrective push notification.

medium impact low prob integration

FCM and APNs have different payload structures and size limits. An oversized or malformed payload could cause silent notification drops on iOS or delivery failures on Android, meaning mentors never learn their summary is ready.

Mitigation & Contingency

Mitigation: Build the PushNotificationDispatcher with separate FCM and APNs payload constructors, enforce a 256-byte body limit on the preview text, and run integration tests against the Firebase Emulator and a test APNs sandbox.

Contingency: Fall back to a generic 'Your periodic summary is ready' message if personalised preview text construction fails, ensuring delivery even when the personalisation pipeline encounters an error.

medium impact high prob scope

Outlier thresholds that are too tight will flag most mentors as outliers (alert fatigue for coordinators), while thresholds that are too loose will miss genuinely underactive mentors — directly undermining HLF's follow-up goal.

Mitigation & Contingency

Mitigation: Implement thresholds as configurable per-organisation database settings rather than hardcoded constants. Provide sensible defaults (underactive < 2 sessions/period, overloaded > 20 sessions/period) and document the tuning process for coordinators in the admin portal.

Contingency: If coordinators report threshold miscalibration after launch, expose a threshold configuration UI in the coordinator admin screen and allow real-time threshold adjustment without requiring a code deployment.

low impact high prob scope

The app may not have 12 months of historical activity data for all organisations at launch, making year-over-year comparison impossible for most users and rendering the comparison widget empty, which could disappoint users expecting Wrapped-style insights.

Mitigation & Contingency

Mitigation: Design the generation service to gracefully handle missing prior-year data by setting the yoy_delta field to null rather than zero. The UI must treat null as 'no comparison available' with appropriate placeholder copy rather than showing a misleading 0% delta.

Contingency: If historical data import from legacy Excel/Word sources becomes feasible, add a one-time backfill Edge Function that populates prior-year activity records from imported spreadsheets. Until then, explicitly communicate the data-availability limitation in the first summary each user receives.