high priority medium complexity integration pending integration specialist Tier 2

Acceptance Criteria

Outbound webhook payload is serialised as JSON conforming to the agreed canonical schema: `{ event_id, event_type, mentor_id, certification_status, expiry_date, renewal_history_snapshot, emitted_at }`
All outbound payloads include a HMAC-SHA256 signature header (`X-Webhook-Signature`) computed from the payload body using a shared secret stored in the credential vault
Inbound webhooks from HLF Dynamics include a signature header that is verified server-side before processing — payloads with invalid or missing signatures are rejected with HTTP 401
Idempotency: before processing any inbound webhook, `event_id` is checked against `cert_notification_log` table; duplicate `event_id` returns HTTP 200 (acknowledged) without reprocessing
`cert_notification_log` entry is created for every successfully processed inbound webhook with fields: event_id, received_at, processed_at, processing_status
`renewal_history_snapshot` field is limited to the last 5 renewal events to prevent unbounded payload growth
Dates are serialised as ISO 8601 UTC strings (`2026-03-26T00:00:00Z`) in all payloads
`certificationStatus` enum values in outbound payloads map exactly to the values expected by HLF Dynamics portal (confirmed with HLF integration spec)
Signature verification uses constant-time comparison to prevent timing attacks
Unit test: duplicate event_id → returns 200 without inserting a second log row
Unit test: invalid HMAC signature → returns 401 and logs rejection event

Technical Requirements

frameworks
Flutter
Dart
apis
Microsoft Dynamics 365 REST API
Supabase PostgreSQL 15
Supabase Edge Functions (Deno)
data models
certification
annual_summary
performance requirements
Idempotency check (database lookup by event_id) must complete within 100ms
HMAC computation must complete within 10ms for typical payload sizes
Webhook processing endpoint must respond within 5 seconds to avoid Dynamics portal timeout
security requirements
HMAC shared secret stored in Edge Function environment secrets — never in mobile app
Constant-time HMAC comparison to prevent timing attacks (use crypto.timingSafeEqual in Deno)
National identity numbers (personnummer) must NOT appear in webhook payloads
cert_notification_log table RLS: service role access only, no mobile client access
Webhook endpoint protected by signature verification before any database read/write
event_id must be a UUID generated server-side to prevent client-controlled idempotency bypass

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Define a `WebhookPayload` Dart class with `toJson()` / `fromJson()` using `json_serializable`. The HMAC signing should be encapsulated in a `WebhookSigner` utility class that takes the raw body bytes and the secret, returning the hex-encoded HMAC-SHA256 digest. For Deno Edge Functions, use the built-in `crypto.subtle.sign()` with HMAC-SHA256. The `cert_notification_log` table needs a unique index on `event_id` to enforce idempotency at the database level (not just application level) — this prevents race conditions on concurrent delivery.

Use `INSERT ... ON CONFLICT (event_id) DO NOTHING` and check `rowsAffected` to detect duplicates. The `renewal_history_snapshot` should be computed at serialisation time by sorting renewal records by `issued_at` descending and taking the first 5. Keep the webhook payload schema in a shared Dart model file so both the Edge Function and any Flutter-side preview code use the same definition.

Testing Requirements

Unit tests with flutter_test covering: correct JSON serialisation of all payload fields including ISO 8601 dates, HMAC signature generation produces consistent output for identical inputs, duplicate event_id lookup returns early without DB write, invalid signature → 401 response, missing signature header → 401 response, renewal_history_snapshot truncated to 5 entries when source has more. Integration test against sandbox: end-to-end outbound webhook delivery with valid signature accepted by Dynamics sandbox, inbound webhook from Dynamics sandbox processed and logged. Test the constant-time comparison to ensure it handles both valid and invalid signatures without timing variance.

Component
HLF Dynamics Sync Service
service medium
Epic Risks (3)
high impact medium prob integration

HLF Dynamics portal webhook API contract may be undocumented, subject to change, or require a separate authentication flow not yet agreed upon with HLF. If the contract changes post-implementation, the sync service silently fails and expired peer mentors remain on public listings.

Mitigation & Contingency

Mitigation: Obtain the official Dynamics webhook specification and test credentials from HLF before starting HLFDynamicsSyncService implementation. Agree on a versioned webhook contract and request a staging endpoint for integration testing.

Contingency: If the contract is unavailable, stub the sync service behind a feature flag and ship without Dynamics sync initially. Queue sync events locally and replay once the contract is confirmed.

high impact medium prob security

Supabase RLS policies for certifications must correctly scope data to the coordinator's chapter without leaking cross-organisation data, particularly complex in multi-chapter membership scenarios. A misconfigured policy could expose peer mentor PII to wrong coordinators.

Mitigation & Contingency

Mitigation: Write RLS policies against the established org-hierarchy schema used by other tables. Peer review all policies before migration deployment. Add integration tests that assert cross-organisation data isolation using test accounts with different org scopes.

Contingency: If a policy gap is discovered post-merge, immediately disable the affected query endpoint and apply a hotfix migration. Audit access logs in Supabase for any cross-org data access events.

medium impact low prob technical

Storing renewal history as a JSONB field rather than a normalised table simplifies queries but makes retrospective schema changes (adding fields to history entries) harder and could cause issues if history grows very large for long-tenured mentors.

Mitigation & Contingency

Mitigation: Define a versioned JSONB entry schema (include a schema_version field in each entry) so future migrations can transform old entries. Add a size guard in the repository to warn if renewal_history exceeds 500 entries.

Contingency: If JSONB approach proves limiting, add a normalised certification_renewal_events table and migrate history entries in a background job, keeping the JSONB field as a read cache.