critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

First call to fetchOrganizations() within a session executes exactly one Supabase network request and stores the result in memory keyed by the authenticated user's UID
All subsequent calls to fetchOrganizations() within the same session return the cached list without issuing any Supabase query (verified via mock/spy asserting zero additional calls)
Signing out via Supabase Auth triggers cache invalidation so the in-memory list is cleared immediately
Signing in as a different user within the same app process returns that user's organization list, not the previous user's cached list
Cache is implemented as a private field (Map<String, List<Organization>>) — never exposed publicly
OrganizationRepository constructor accepts a SupabaseClient dependency for testability
Repository compiles without warnings under `dart analyze` and `flutter_test` passes all new tests
If the network call fails on the first fetch, no partial/empty list is cached; next call retries the network

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
apis
Supabase PostgreSQL (select on organizations table)
Supabase Auth (onAuthStateChange stream)
data models
assignment
performance requirements
Cache lookup must complete in O(1) — Map lookup by user UID string key
Memory footprint: organization list is expected to be small (< 50 orgs per user); no eviction policy needed
Cache hit must return synchronously (no async overhead) — use synchronous map access wrapped in Future.value()
security requirements
Cache key must be the Supabase Auth UID, never an email or display name
Cache is process-local and never written to disk — no risk of cross-restart leakage
On auth sign-out event, cache entry for that UID must be deleted before the sign-out Future completes
Row-Level Security on the organizations table provides server-side enforcement; client cache is a performance layer only

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use a private `final Map> _cache = {}` field inside OrganizationRepository. Subscribe to `supabaseClient.auth.onAuthStateChange` in the constructor; when the event type is `AuthChangeEvent.signedOut`, remove the entry for the signed-out user's UID. For the fetch method, check `_cache[uid]` first; if non-null return `Future.value(_cache[uid]!)`, otherwise await the Supabase query, store the result, and return it. Keep the auth subscription in a `StreamSubscription` and expose a `dispose()` method (or rely on Riverpod provider disposal) to cancel it.

Avoid using a global/singleton cache — the repository instance itself is the scope boundary, managed by Riverpod.

Testing Requirements

Write unit tests in flutter_test covering: (1) first fetch hits network and populates cache, (2) second fetch returns cached value with zero additional network calls (use Mockito mock of SupabaseClient), (3) sign-out event clears the correct cache entry while leaving other users' entries intact, (4) fetch failure leaves cache empty and next call retries. Aim for 100% branch coverage on the cache logic. No integration tests required at this layer — mocked Supabase client is sufficient.

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.