Build in-memory and persistent local cache with TTL
epic-organization-feature-flags-foundation-task-005 — Implement the FeatureFlagCache component providing a two-layer cache: a short-lived in-memory Map for the current app session and a persistent local store (e.g., shared_preferences or Hive) for offline resilience. Support configurable TTL per flag, cache invalidation on org switch, and a read-through fallback so startup works without network connectivity.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Use `DateTime.now()` injection via a `Clock` interface (or simple `DateTime Function()` typedef) rather than calling `DateTime.now()` directly — this makes TTL expiry fully testable without real time delays. Store the expiry timestamp alongside the value in the persistent layer as a JSON string: `{"v": true, "exp": 1748700000}`. For the in-memory layer, use a `Map
Key namespacing pattern: `ff_v1_${orgId}_${flagKey}` — include a version prefix so cache entries can be invalidated wholesale if the schema changes. Register as a Riverpod `Provider` so it can be injected into the repository integration layer (task-007).
Testing Requirements
Unit tests using `flutter_test` with a fake clock (`FakeClock` or manual `DateTime` injection) to control TTL expiry without real time delays. Test cases: (1) put then get within TTL returns value; (2) put then get after TTL expiry returns null; (3) in-memory eviction falls through to persistent layer correctly; (4) invalidate(orgId) clears only target org entries, leaving other org entries intact; (5) clear() wipes all entries; (6) putAll writes all flags and each is retrievable; (7) concurrent put/get calls do not cause state corruption (Dart single-threaded but verify async ordering). Use a fake SharedPreferences instance (`SharedPreferences.setMockInitialValues({})`) so tests do not touch real device storage. Minimum 95% line coverage on the cache class.
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.