high priority medium complexity integration pending integration specialist Tier 5

Acceptance Criteria

An FCM push notification is dispatched server-side for every badge awarded in the edge function, within the same invocation
Notification title is the badge name (retrieved from badge_definition.name) in the user's organization language
Notification body contains a fixed motivational message referencing the badge name, e.g. 'Du har fått merket [Badge Name]! Flott innsats!'
Notification data payload includes a deep link URI formatted as `app://badges/{badge_id}` for the Flutter app to handle on tap
FCM dispatch is performed server-side only using the FCM API v1 via Supabase Edge Function — no FCM server key is present in the Flutter app binary
Device token is retrieved from the device_token table filtered by user_id = peer_mentor_id and platform IN ('ios', 'android') before dispatch
If the peer mentor has no registered device token, the notification step is skipped silently and logged at DEBUG level
If FCM returns a 404 (invalid token), the stale token is deleted from the device_token table automatically
FCM dispatch failure does not cause the edge function to return an error — badge award is considered successful regardless of notification delivery
Notification delivery latency from activity save to push receipt on device must be under 5 minutes under normal conditions, verified during integration testing

Technical Requirements

frameworks
Supabase Edge Functions (Deno runtime)
Firebase Cloud Messaging (FCM) API v1
apis
FCM API v1 (https://fcm.googleapis.com/v1/projects/{project_id}/messages:send)
Supabase PostgreSQL (device_token table read/delete)
data models
device_token
badge_definition
performance requirements
FCM HTTP call must complete within 3 seconds per notification; use a 3-second timeout on the fetch call
Device token lookup must use an indexed query on user_id (device_token table must have index on user_id)
FCM dispatch for multiple badges awarded in one run must be parallelized using Promise.allSettled
security requirements
FCM service account key must be stored in Supabase Edge Function environment secrets, never in the mobile app binary
Notification payload must not include contact names, activity content, or any PII beyond the badge name
deep link URI must be validated to match the expected `app://badges/{uuid}` pattern before inclusion in payload
Stale token cleanup (404 response) must use service role to delete from device_token table
FCM project_id must be stored in environment variables, not hardcoded

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Use FCM API v1 (not legacy FCM) which requires a Google service account OAuth2 token — generate this server-side using the service account JSON stored in Deno environment secrets. The OAuth2 token has a 1-hour TTL; cache it in memory within the Deno isolate for reuse across warm invocations (use a module-level variable with an expiry check). Retrieve badge_definition.name and badge_definition.icon_key from the database before dispatching. Use `Promise.allSettled` when dispatching multiple notifications for a single invocation (multiple badges awarded).

Implement stale token cleanup by checking FCM response for error code `UNREGISTERED` (404) and deleting the matching row from device_token. Keep notification body text short (under 100 characters) for visibility on lock screen. Define the deep link scheme `app://badges/{id}` and confirm with the Flutter team that the app handles this route via GoRouter.

Testing Requirements

Unit tests: (1) mock FCM HTTP call returning 200 → assert notification payload contains correct title, body, and data deep link, (2) no device token for mentor → assert FCM not called and no error thrown, (3) FCM returns 404 → assert stale device_token row deleted and no error propagated, (4) FCM throws network error → assert badge award still treated as successful and error logged. Integration test: award a test badge, verify FCM HTTP call was made with correct Authorization header (Bearer {OAuth token}) and correct message structure. Latency test: measure time from activity insert trigger to FCM call in staging environment and assert under 5 minutes.

Component
Badge Criteria Edge Function
infrastructure medium
Epic Risks (3)
medium impact medium prob technical

Supabase Edge Functions may experience cold start latency of 500ms–2s when they have not been invoked recently. If evaluation latency consistently exceeds the 2-second UI expectation, the celebration overlay timing SLA cannot be met without the optimistic UI fallback from the UI epic.

Mitigation & Contingency

Mitigation: Keep the edge function warm by scheduling a lightweight health-check invocation every 5 minutes in production. Optimise the function size to minimise Deno module load time. Implement the optimistic UI path in badge-bloc (from the UI epic) as the primary UX path so cold start only affects server-side reconciliation, not perceived responsiveness.

Contingency: If cold starts remain problematic, migrate badge evaluation to a Supabase database function (pl/pgsql) triggered directly by a database trigger on activity insert, eliminating the Edge Function overhead entirely for the evaluation logic while keeping Edge Function only for FCM notification dispatch.

high impact low prob integration

Supabase database webhooks can fail silently if the edge function returns a non-2xx response or times out. A missed webhook means a peer mentor does not receive a badge they earned, which is both a functional defect and a trust issue for organisations relying on milestone tracking.

Mitigation & Contingency

Mitigation: Implement idempotent webhook processing: the edge function reads the activity ID from the webhook payload and checks whether evaluation for this activity has already run (via an audit log query) before proceeding. Add Supabase webhook retry configuration (3 retries with exponential backoff). Monitor webhook failure rates via Supabase logs alert.

Contingency: Implement a nightly reconciliation job (Supabase scheduled function) that scans all activities from the past 24 hours, re-evaluates badge criteria for any peer mentor with no corresponding evaluation log entry, and awards any missing badges. Alert operations if reconciliation awards more than 5% of badges, indicating systematic webhook failure.

high impact low prob security

The evaluation service loads badge definitions per organisation, but a misconfigured RLS policy or incorrect organisation scoping in the edge function could cause one organisation's badge criteria to be evaluated against another organisation's peer mentor activity data, leading to incorrect or cross-contaminated badge awards.

Mitigation & Contingency

Mitigation: The edge function must extract organisation_id from the webhook payload activity record and pass it explicitly to every database query. Write a security test that seeds two organisations with distinct badge definitions and verifies that evaluating a peer mentor in org A never reads or awards org B definitions. Use Supabase service role key only within the edge function, never the anon key.

Contingency: If cross-org contamination is detected in audit logs, immediately disable the edge function webhook, run a targeted SQL query to identify and revoke incorrectly awarded badges, notify affected organisations, and perform a full security review of all RLS policies on badge-related tables before re-enabling.