Implement Supabase realtime subscription for activities table
epic-activity-statistics-dashboard-state-and-realtime-task-004 — Set up a Supabase realtime channel subscription on the activities table within the Stats Cache Invalidator component. Subscribe to INSERT and UPDATE events scoped to the current user's organization and chapter using RLS-aligned filter parameters. The subscription must be established when the invalidator provider is first read and torn down when the provider is disposed, following Riverpod lifecycle best practices with ref.onDispose.
Acceptance Criteria
Technical Requirements
Execution Context
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.
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.
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.