critical priority medium complexity integration pending integration specialist Tier 4

Acceptance Criteria

Stats Cache Invalidator holds a stable `WidgetRef` or `Ref` reference that survives Supabase realtime events without re-subscribing
The invalidator calls `ref.invalidate(statsNotifierProvider(chapterId, timeWindow))` with correct scoped parameters after debounce
Provider family instance (scoped by chapterId + timeWindow) is passed to the invalidator at construction time and never changes mid-lifecycle
After a Supabase realtime INSERT event on the activities table, the StatsAsyncNotifier transitions through AsyncLoading then resolves to AsyncData without any user interaction
The invalidator does not hold strong references to BuildContext — uses only Riverpod Ref
Re-subscription to the Supabase realtime channel does not occur on each invalidation — only on initial setup
If the stats provider family has been disposed before invalidation fires, the invalidate call is a no-op and does not throw
The wiring is tested end-to-end: realtime event → debounce → invalidate → re-fetch confirmed via state transitions

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase Realtime (WebSocket)
data models
activity
annual_summary
performance requirements
Invalidation must trigger notifier re-fetch within 50ms of debounce expiry
Realtime subscription must not be re-established on each invalidation cycle
Provider invalidation must not cause full widget tree rebuild — only listening widgets should rebuild
security requirements
Realtime channel subscription must be scoped to the authenticated user's organisation via Supabase RLS
JWT must be validated on each Supabase Realtime channel subscription per security policy
No sensitive PII from realtime payload must be forwarded — only row IDs and status fields per integration security rules

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Inject the invalidator with a `Ref` (not `BuildContext`) using Riverpod's `ref.read` pattern inside a non-UI provider or using `riverpod_annotation`. Pass the specific provider family arguments (chapterId, timeWindow) as constructor parameters to the invalidator at instantiation time — do not capture them via closure at invalidation time, as they may have changed. Use `ref.invalidate` (Riverpod 2.x) rather than the deprecated `ref.refresh` to avoid triggering an immediate synchronous fetch. Guard the `ref.invalidate` call with a try/catch or check `ref.exists(provider)` if Riverpod exposes it.

Keep the Supabase channel subscription in a separate service layer — the invalidator only handles the ref.invalidate call, not the subscription itself.

Testing Requirements

Write integration tests using flutter_test with ProviderContainer: (1) inject a mock Supabase realtime stream; (2) emit an INSERT event; (3) advance fake time by 800ms; (4) assert that statsNotifierProvider state is AsyncLoading then AsyncData; (5) assert re-fetch was called exactly once on the repository mock. Also test: invalidate called after provider disposal does not throw. Use ProviderContainer.overrides to replace the Supabase realtime dependency with a StreamController.

Component
Stats Cache Invalidator
infrastructure medium
Epic Risks (2)
medium impact medium prob technical

Supabase realtime channel subscriptions that are not properly disposed on screen close can accumulate in memory across navigation events, causing duplicate invalidation calls, ghost fetches, and eventual memory leaks on long sessions.

Mitigation & Contingency

Mitigation: Implement StatsCacheInvalidator as a Riverpod provider with an explicit ref.onDispose callback that cancels the realtime channel subscription. Write a widget test that navigates away and back multiple times and asserts that only one subscription is active at any given time.

Contingency: If subscription leaks are found in production, add a global subscription registry that enforces at-most-one subscription per channel key, and schedule a dispose sweep on app background events.

medium impact low prob scope

Debouncing rapid inserts may swallow the invalidation signal if the debounce window outlasts the Supabase realtime event delivery window, resulting in the dashboard showing stale totals after a bulk registration completes.

Mitigation & Contingency

Mitigation: Set the debounce window to 800ms (shorter than the typical Supabase realtime delivery latency of 1-2s for batched events) and ensure the leading-edge invalidation fires immediately while trailing duplicates are suppressed. Integration-test with a 20-record bulk insert.

Contingency: If debounce timing proves unreliable, replace debounce with a trailing-edge timer reset on each event and add a guaranteed invalidation 5 seconds after the last event regardless of subsequent events.