critical priority medium complexity infrastructure pending infrastructure specialist Tier 2

Acceptance Criteria

StatsCacheInvalidator is implemented as a Riverpod provider (not a widget) that sets up the realtime channel in its initialization
The channel subscribes to INSERT and UPDATE events on the activities table — DELETE events are intentionally excluded unless business rules require them
Channel filter parameters include organization_id and chapter_id aligned with the authenticated user's RLS context, preventing cross-organization event leakage
On receiving any subscribed event, the invalidator calls statsAsyncNotifierProvider.invalidate() (or equivalent) to trigger a fresh fetch
ref.onDispose is registered to unsubscribe and remove the Supabase channel, preventing memory leaks and stale subscriptions after logout
If the Supabase realtime connection drops and reconnects, the channel re-subscribes automatically (Supabase client handles this — verify the behavior is not blocked by the implementation)
The invalidator provider is kept alive for the duration of the stats dashboard screen — it must not be auto-disposed between tab switches
Integration test verifies: inserting a row into activities via Supabase triggers a state re-fetch in StatsAsyncNotifier within 2 seconds
No duplicate channel registrations occur if the provider is read multiple times (channel is created once)
Logs a structured debug message (not print) when a realtime event is received and when the channel is torn down

Technical Requirements

frameworks
flutter
riverpod
supabase_flutter
apis
Supabase Realtime (channel subscription API)
statsAsyncNotifierProvider (internal)
data models
activities (Supabase table)
RealtimePostgresChangesPayload
performance requirements
Realtime event processing (receive → invalidate call) must complete in under 100ms
Channel setup must not block the UI thread — use async initialization
No more than one active channel per authenticated session for the activities table stats subscription
security requirements
Channel filter must include eq(organization_id, currentOrgId) to prevent receiving events from other organizations
Do not log the full event payload — log only event type and timestamp to avoid leaking PII from activity records
Verify Supabase RLS policies also restrict realtime events server-side as defense-in-depth

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use supabase.channel('stats-invalidator-${orgId}-${chapterId}') with a unique name per scope to avoid channel conflicts in multi-tab or multi-session scenarios. The filter syntax for Supabase realtime is: `.onPostgresChanges(event: PostgresChangeEvent.insert, schema: 'public', table: 'activities', filter: PostgresChangeFilter(type: FilterType.eq, column: 'organization_id', value: orgId), callback: ...)`. Chain `.subscribe()` after setting up event handlers. Store the channel reference in a local variable inside the provider's build and register teardown with `ref.onDispose(() => supabase.removeChannel(channel))`.

Use `keepAlive: true` on the provider (via `ref.keepAlive()` in the build body) so the subscription persists while the stats screen is in the navigation stack but the specific notifier is not the active widget. Avoid using StreamController as a wrapper — the Supabase client's callback-based API is sufficient.

Testing Requirements

Two levels of testing required. Unit tests: mock the Supabase client's channel() method and verify (1) the channel is created with correct filter parameters on provider initialization, (2) receiving a mock INSERT payload calls statsNotifier.invalidate(), (3) disposing the provider calls channel.unsubscribe(). Integration test (optional, against local Supabase): insert a row into activities, verify that StatsAsyncNotifier transitions to AsyncLoading and then AsyncData within 2 seconds. Use flutter_test for unit tests.

For integration, use a dedicated test Supabase project with seeded data. Mark integration tests with a @Tags(['integration']) annotation so they can be excluded from fast CI runs.

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.