critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

A `FeatureFlagCache` class exists with `get(String orgId, String flagKey)`, `put(String orgId, String flagKey, bool value, {Duration ttl})`, `putAll(String orgId, Map<String, bool> flags, {Duration ttl})`, `invalidate(String orgId)`, and `clear()` methods
`get` returns the cached value if it exists in the in-memory layer and is within TTL; returns null on cache miss or expiry
On in-memory miss, `get` falls through to the persistent layer (shared_preferences) and re-populates in-memory if a valid non-expired entry is found
On persistent-layer miss or expired entry, `get` returns null so the caller knows to fetch from Supabase
`put` and `putAll` write to both the in-memory and persistent layers atomically
`invalidate(orgId)` removes all cache entries for the given organization from both layers without affecting other organizations' cached data
`clear()` wipes all feature flag cache entries from both layers (used on logout)
Default TTL is 15 minutes for all flags unless overridden; TTL is stored alongside the value so expiry is deterministic
Cache keys are namespaced to avoid collision with other shared_preferences data (e.g., prefix `ff_v1_{orgId}_{flagKey}`)
All methods are safe to call from the UI thread (async, non-blocking)

Technical Requirements

frameworks
Dart (latest)
Flutter
shared_preferences
performance requirements
In-memory `get` must complete synchronously (O(1) Map lookup) — no async overhead on cache hit
Persistent layer writes are fire-and-forget where possible (do not block the call-site waiting for disk write)
Cache key namespace prefix must be short to minimise shared_preferences key storage overhead
security requirements
Feature flag values are non-sensitive (boolean capability flags) and do not require encrypted storage
The cache must not store any PII, JWT tokens, or user credentials — only flag key/value/expiry triples
On user logout, `clear()` must be called to prevent stale org flag state being visible to the next user on a shared device

Execution Context

Execution Tier
Tier 1

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` where `_CacheEntry` holds the value and expiry DateTime. The `shared_preferences` package is already likely in the Flutter project; if Hive is not already a dependency, prefer `shared_preferences` to avoid adding a new dependency.

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.

Component
Feature Flag Local Cache
data low
Epic Risks (3)
high impact medium prob security

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.

medium impact medium prob technical

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.

medium impact low prob technical

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.