high priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

OrganizationRepository exposes a `Stream<List<Organization>> get organizationsStream` getter
A new subscriber receives the current cached organization list as the first emitted event without a network request (BehaviorSubject-style replay)
When the Supabase Realtime channel emits an INSERT, UPDATE, or DELETE event on the organizations table, the in-memory cache is updated and the stream emits the new list within 500 ms
The stream does not emit duplicate consecutive lists (equality check prevents redundant UI rebuilds)
Riverpod StreamProvider watching this stream rebuilds only on actual data changes
Supabase Realtime channel is subscribed with RLS-filtered scope — only rows visible to the authenticated user are received
On sign-out, the Realtime channel is unsubscribed and the stream emits an empty list or closes cleanly
The stream does not leak: cancelling all subscriptions causes the underlying channel to be removed from Supabase Realtime

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
apis
Supabase Realtime (channel subscription on 'organizations' table, filter: postgres_changes)
Supabase Auth (onAuthStateChange for cleanup on sign-out)
data models
assignment
performance requirements
Stream emission from Realtime event to UI rebuild must complete within 500 ms under normal network conditions
Use a StreamController.broadcast() or rxdart BehaviorSubject to support multiple simultaneous Riverpod listeners without multiplied network subscriptions
Unsubscribe Realtime channel when listener count drops to zero to conserve websocket connections
security requirements
Supabase Realtime channel JWT is validated server-side; RLS ensures only authorized rows trigger events on the client
No PII is transmitted in the Realtime payload beyond row IDs and status fields — full data fetched via normal Supabase query if needed
Channel subscription must include the authenticated user's JWT so RLS applies to the subscription itself

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use `rxdart`'s `BehaviorSubject>` as the internal sink — it naturally replays the latest value to new subscribers. In the constructor, after the initial fetch populates the cache, add the list to the subject. Subscribe to `supabase.channel('organizations').onPostgresChanges(...)` and on each event, fetch the updated row (or re-query the full list if simpler), update the cache, and add to the subject. Keep a ref-count or use `shareValueSeeded` to manage the Realtime channel lifecycle.

Expose the stream as `organizationsStream` via `subject.stream`. In Riverpod, wire via `StreamProvider.autoDispose` so the channel is torn down when no widgets are listening. Ensure the Realtime filter uses `schema: 'public', table: 'organizations'` with `event: '*'`.

Testing Requirements

Write unit tests in flutter_test with a fake/mock RealtimeChannel: (1) verify stream emits cached value immediately on subscription, (2) simulate an INSERT payload and assert stream emits updated list, (3) simulate sign-out and assert stream emits empty list or closes, (4) assert subscribing twice reuses the same channel (no duplicate channels). Write one integration test (or widget test with fakeAsync) confirming a Riverpod StreamProvider watching organizationsStream rebuilds on stream emission.

Component
Organization Repository
data medium
Epic Risks (2)
high impact low prob technical

TenantSessionStore must write to both SecureStorageAdapter and Supabase session synchronously. If the Supabase write succeeds but the local secure storage write fails (or vice versa), the system ends up in an inconsistent state: local app thinks org A is selected but Supabase queries are scoped to org B (or unscoped), causing silent data leakage or empty result sets.

Mitigation & Contingency

Mitigation: Implement the dual-write with compensating rollback: if the second write fails, undo the first write and surface a typed DualWriteFailureError to callers. Add a startup integrity check in restoreSession() that validates local storage and Supabase session agree on the current org_id.

Contingency: If integrity check fails on startup, clear both stores and redirect the user to the org selection screen rather than proceeding with potentially mismatched state.

medium impact low prob integration

An organization could be deactivated in Supabase between the time the org list is cached and the time the user taps to select it. If the repository serves stale cached data the org-selection-service will attempt to seed a context for an inactive org, potentially causing RLS scope issues or confusing error states.

Mitigation & Contingency

Mitigation: The OrganizationRepository.getOrganizationById() path used during selection validation always performs a fresh network fetch (bypassing cache) to confirm the org is still active before the TenantSessionStore writes anything.

Contingency: If a freshness check finds the org is inactive, surface a localized error message on the selection screen ('This organization is no longer available') and refresh the org list to show only currently active partners.