critical priority medium complexity infrastructure pending infrastructure specialist Tier 3

Acceptance Criteria

A Supabase Edge Function named `expiry-check` is created at `supabase/functions/expiry-check/index.ts` and deployable via `supabase functions deploy expiry-check`
The function queries the `certification` table for all records where `expires_at <= NOW() + INTERVAL '60 days'` AND `status` is active, returning `peer_mentor_id`, `cert_type`, `expires_at`, and `organization_id`
Results are partitioned into four tiers: `lapsed` (expires_at < NOW()), `7-day` (0–7 days), `30-day` (8–30 days), `60-day` (31–60 days) — a certification belongs to exactly one tier
The `lapsed` partition is passed to both the notification orchestrator AND the visibility suppressor service
The `7-day`, `30-day`, and `60-day` partitions are passed to the notification orchestrator only
The function returns HTTP 200 with a JSON summary `{ tiersProcessed: { lapsed: N, sevenDay: N, thirtyDay: N, sixtyDay: N }, errors: [] }` on full success
If downstream service calls fail for a tier, the function continues processing remaining tiers and records failures in the `errors` array of the summary response
Multi-tenant isolation is enforced: the query uses the service-role key but includes `organization_id` in all downstream calls so each organisation's data remains scoped
The function handles an empty result set (no expiring certifications today) gracefully, returning a summary with all tier counts at 0

Technical Requirements

frameworks
Deno (Supabase Edge Function runtime)
Supabase Edge Functions TypeScript
apis
Supabase service-role PostgreSQL client for certification expiry query
Internal Supabase Edge Function invocation for orchestrator and visibility suppressor
data models
certification (peer_mentor_id, cert_type, expires_at, status, organization_id)
assignment (status — to scope lapsed suppression to active assignments only)
performance requirements
Certification query must use a database index on `expires_at` — verify index exists before deployment
Tier partitioning done in-memory after a single database query — no separate query per tier
Downstream service calls for all tiers dispatched in parallel via `Promise.allSettled`
Total function execution time must stay within Supabase Edge Function's 60-second wall-clock limit for up to 10 000 certification records
security requirements
Function only invocable via Supabase pg_cron internal scheduler or with a valid service-role `Authorization` header — no public unauthenticated access
Service-role key injected via Deno environment variable `SUPABASE_SERVICE_ROLE_KEY` — never hardcoded
Organisation context included in all downstream payloads to enforce multi-tenant isolation across all services called
Function does not return raw PII in its HTTP response — only aggregate counts and error messages

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Structure the function as a pipeline: `query → partition → dispatch`. Keep each stage in a separate async function to make the flow readable and independently testable. The tier boundary comparisons should use UTC timestamps exclusively — never local time — since Supabase runs in UTC. Use `supabase.from('certification').select(...)` with `.lte('expires_at', sixtyDayBoundary)` to retrieve all candidates in a single query, then partition in JavaScript.

Define the tier boundaries as named constants at the top of the file (`LAPSED_BOUNDARY`, `SEVEN_DAY_BOUNDARY`, etc.) so they are easy to adjust. For dispatching to downstream services, use Supabase's `functions.invoke` method rather than raw `fetch` to benefit from automatic auth header injection.

Testing Requirements

Integration tests using Deno test runner with a mocked Supabase client. Required scenarios: (1) certifications spread across all four tiers are correctly partitioned — verify counts match; (2) lapsed certifications trigger calls to BOTH orchestrator and visibility suppressor; (3) non-lapsed tiers trigger orchestrator call only; (4) empty database result returns 200 with all-zero counts; (5) downstream orchestrator failure for one tier does not prevent other tiers from being processed; (6) function returns HTTP 200 with error summary when partial failures occur. Additionally write a manual smoke-test checklist for staging: verify pg_cron fires, check execution log table, confirm notification records created.

Component
Certificate Expiry Check Edge Function
infrastructure medium
Epic Risks (4)
high impact medium prob technical

If the daily edge function runs more than once in a 24-hour window due to a Supabase scheduling anomaly or manual re-trigger, the orchestrator could dispatch duplicate push notifications to the same mentor and coordinator for the same threshold, eroding user trust.

Mitigation & Contingency

Mitigation: Implement idempotency at the notification record level using a unique constraint on (mentor_id, threshold_days, certification_id). The orchestrator checks for an existing record before dispatching. Use a database-level upsert with ON CONFLICT DO NOTHING.

Contingency: If duplicate notifications are reported in production, add a rate-limiting guard in the edge function that aborts if a notification for the same mentor and threshold was created within the last 20 hours, and add an alerting rule to Supabase logs for duplicate dispatch attempts.

medium impact medium prob scope

The mentor visibility suppressor relies on the daily edge function to detect expiry and update suppression_status. A mentor whose certificate expires at midnight may remain visible for up to 24 hours if the cron runs at a fixed time, violating HLF's requirement that expired mentors disappear promptly.

Mitigation & Contingency

Mitigation: Schedule the edge function to run at 00:05 UTC to minimise lag after midnight transitions. Additionally, the RLS policy can include a direct date comparison (certification_expiry_date < now()) as a secondary predicate that does not rely on suppression_status, providing real-time enforcement at the database level.

Contingency: If the cron lag is unacceptable after launch, implement a Supabase database trigger on the certifications table that fires on UPDATE of expiry_date and calls the suppressor immediately, reducing lag to near-zero for renewal and expiry events.

medium impact low prob integration

The orchestrator needs to resolve the coordinator assigned to a specific peer mentor to dispatch coordinator-side notifications. If the assignment relationship is not normalised or is missing for some mentors, coordinator notifications will silently fail.

Mitigation & Contingency

Mitigation: Query the coordinator assignment from the existing assignments or user_roles table before dispatch. Log a structured warning (missing_coordinator_assignment: mentor_id) when no coordinator is found. Add a data quality check in the edge function that reports mentors without coordinators.

Contingency: If coordinator assignments are missing at scale, fall back to notifying the chapter-level admin role for the mentor's chapter, and surface a data quality report to the admin dashboard showing mentors without assigned coordinators.

medium impact low prob dependency

The course enrollment prompt service generates deep-link URLs targeting the course administration feature. If the course administration feature changes its deep-link schema or the Dynamics portal URL structure changes, enrollment prompts will navigate to broken destinations.

Mitigation & Contingency

Mitigation: Define the deep-link contract between the certificate expiry feature and the course administration feature as a shared constant in a cross-feature navigation config. Version the deep-link schema and validate the generated URL format in unit tests.

Contingency: If the deep-link breaks in production, the course enrollment prompt service should gracefully fall back to opening the course administration feature root screen with a query parameter indicating the notification context, allowing the user to manually locate the correct course.