critical priority low complexity database pending database specialist Tier 0

Acceptance Criteria

SummaryCacheRepository class is implemented with setCachedSummary, getCachedSummary, invalidateCache, and invalidateOrganisationCache methods
setCachedSummary stores summary payload (as JSON), cache key, organisation_id, period_id, and expires_at in Supabase
getCachedSummary returns null (not throws) for cache misses and for entries where expires_at is in the past
getCachedSummary does NOT return stale entries — expiry check is applied server-side in the Supabase query (WHERE expires_at > now()) to prevent race conditions
invalidateCache deletes a specific cache entry by cache_key + organisation_id
invalidateOrganisationCache deletes all cache entries for a given organisation_id (used when underlying data changes)
TTL is configurable per cache entry at write time and defaults to 24 hours if not provided
Cache keys are organisation-scoped: the key format is '{organisation_id}:{period_type}:{period_id}' to prevent cross-organisation cache pollution
Repository surfaces typed exceptions: SummaryCacheWriteException, SummaryCacheReadException
All methods are covered by integration tests and the cache key format is validated in unit tests

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Dart SDK
apis
Supabase REST API
data models
SummaryCacheEntry
CachedSummaryPayload
SummaryPeriod
performance requirements
getCachedSummary completes in under 100ms for a cache hit (index on cache_key + organisation_id required)
invalidateOrganisationCache must complete even for organisations with 10,000+ cache entries — use DELETE WHERE, not row-by-row deletion
security requirements
RLS policy on summary_cache table restricts all operations to the authenticated user's organisation_id
Cache payload stored as JSONB — validate structure before inserting to prevent malformed data from being cached
Expired entries must never be returned to callers regardless of client clock skew — rely on server-side now() in queries

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Implement cache storage in a Supabase table: id (uuid pk), cache_key (text), organisation_id (uuid fk), period_id (uuid fk nullable), payload (jsonb), expires_at (timestamptz), created_at. Use a composite unique index on (cache_key, organisation_id) and perform upsert with onConflict on this pair so re-generating a summary updates the cache atomically. For TTL, calculate expires_at = DateTime.now().toUtc().add(ttl) in Dart and pass as an ISO-8601 string to Supabase. Run a Supabase pg_cron job (or application-level scheduled task) to periodically purge expired rows — do not rely solely on query-time filtering to keep the table size bounded.

Register as a Riverpod Provider with SupabaseClient as a dependency. The caching layer is critical for the gamification 'Wrapped' summaries — these may be expensive to compute and must be served instantly from cache when a peer mentor opens their summary screen.

Testing Requirements

Integration tests against Supabase local instance covering: cache set and immediate retrieval (hit), retrieval after TTL expiry (miss), invalidate single entry, invalidateOrganisationCache removing all entries for an org, cross-organisation isolation (org A cannot read org B cache), and setCachedSummary overwrite behaviour (upsert on conflict). Unit tests for key format generation and TTL calculation logic using fake DateTime values.

Component
Summary Cache Repository
data low
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.