Integrate cache layer into repository read path
epic-organization-feature-flags-foundation-task-007 — Wire the FeatureFlagCache into the FeatureFlagRepository so that all read operations check the cache first and only hit Supabase on a cache miss or TTL expiry. Ensure that successful Supabase fetches populate both the in-memory and persistent cache layers. Implement a cache warm-up method called at app startup for offline resilience.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Use the decorator pattern rather than modifying `FeatureFlagRepository` directly — `CachingFeatureFlagRepository` holds a reference to `IFeatureFlagRepository` (the base implementation) and `FeatureFlagCache`, delegating non-cached operations to the base. This preserves the Single Responsibility Principle and keeps the base repository testable in isolation. In the Riverpod provider setup, compose the providers: `cachingRepositoryProvider = Provider((ref) => CachingFeatureFlagRepository(ref.watch(baseRepositoryProvider), ref.watch(featureFlagCacheProvider)))`. For the warm-up freshness check, store a `lastWarmUpAt` timestamp in memory and skip fetching if it is within the TTL window.
Call `warmUp` from the app initialization sequence (e.g., in the root `ConsumerWidget.initState` or in a splash screen), awaiting its completion before navigating to the home screen. Ensure `warmUp` is idempotent — safe to call multiple times without duplicate Supabase requests.
Testing Requirements
Unit tests using `mocktail` to mock both `IFeatureFlagRepository` (base) and `FeatureFlagCache`. Test cases: (1) `getFlag` with cache hit — base repository is NOT called; (2) `getFlag` with cache miss — base repository IS called and result is stored in cache; (3) `setFlag` calls base repository then calls cache invalidate for the specific key; (4) `warmUp` calls `getAllFlags` on base repository then calls `putAll` on cache; (5) `warmUp` with network failure (base repository throws) — method completes without rethrowing; (6) `warmUp` when cache is fresh — base repository is not called (no redundant fetch); (7) org switch triggers invalidation of old org. Integration test: full warm-up → getFlag (cache hit) → setFlag → getFlag (cache miss, Supabase fetched) sequence against local Supabase. Run via `flutter_test`.
Supabase RLS policies for organization_configs may have gaps that allow cross-organization reads if the JWT claim for organization_id is absent or malformed, leading to data leakage between tenants.
Mitigation & Contingency
Mitigation: Implement RLS policies using auth.uid() joined against a memberships table to derive organization_id rather than trusting a client-supplied claim. Write integration tests that simulate a cross-org read attempt and assert it returns zero rows.
Contingency: If a gap is discovered post-launch, immediately disable the affected RLS policy, roll back the migration, and re-implement with a parameterized policy tested against all organization fixture data.
Dart does not have a built-in semantic version comparison library; a naive string comparison (e.g., '2.10.0' < '2.9.0' lexicographically) would cause rollout evaluator to produce incorrect eligibility results for organizations on different app versions.
Mitigation & Contingency
Mitigation: Use the pub.dev `pub_semver` package or implement a proper three-segment integer comparison. Add parameterized unit tests covering 20+ version pairs including double-digit minor/patch segments.
Contingency: If incorrect comparison is discovered in production, push a hotfix with corrected comparison logic and temporarily disable phase-gated flags until all affected organizations have updated to the corrected version.
Persistent local cache written to shared_preferences or Hive could become corrupted or deserialized incorrectly after an app update changes the FeatureFlag schema, causing startup crashes or all flags defaulting to disabled.
Mitigation & Contingency
Mitigation: Wrap all cache reads in try/catch with explicit fallback to the all-disabled default map. Version the cache key (e.g., `feature_flags_v2_{orgId}`) so schema changes automatically invalidate old entries.
Contingency: If cache corruption is detected in a release, publish an app update that clears the versioned cache key on first launch and re-fetches from Supabase.