critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

A `FeatureFlagRepository` class exists with a constructor accepting a `SupabaseClient` dependency (injectable, not singleton-accessed)
`getFlag(String orgId, String flagKey)` returns `Future<bool>` — true if the flag exists and is enabled, false if it does not exist or is explicitly disabled
`getAllFlags(String orgId)` returns `Future<Map<String, bool>>` mapping every flag key to its current boolean value for the given organization
`setFlag(String orgId, String flagKey, bool value)` returns `Future<void>` and performs an upsert on the organization_configs table scoped to the given orgId
All methods only accept flag keys that exist in `FeatureFlagKeys` — passing an unknown string does not silently succeed (throws `ArgumentError` or uses a sealed type)
`getFlag` returns `false` (not an exception) when the flag row does not exist in the database, treating missing as disabled
All methods propagate typed exceptions (`FeatureFlagRepositoryException`) wrapping the underlying `PostgrestException` with an actionable message
Repository does not store or cache state internally — it is a pure data-access layer (caching is in task-005/007)
The class has a corresponding abstract interface (`IFeatureFlagRepository`) to support mocking in tests
Tested with both happy-path and error-path (network failure, RLS rejection) scenarios

Technical Requirements

frameworks
Dart (latest)
Flutter
supabase_flutter
apis
Supabase PostgREST REST API (via supabase_flutter client)
Supabase organization_configs table
data models
organization_configs (feature flags table)
performance requirements
`getAllFlags` must use a single Supabase query (not N individual `getFlag` calls) to minimize round-trips
Queries must filter by organization_id at the database level — do not fetch all rows and filter client-side
security requirements
The Supabase client passed to the repository must use the authenticated user's JWT, not the service role key
RLS on the database (task-002) is the authoritative access control — the repository must not implement redundant client-side org filtering as a security measure
Flag values must not be logged at INFO level — use DEBUG/TRACE only to prevent flag state leaking into log aggregation systems
orgId parameter must be validated as a non-empty UUID before being used in a query to prevent injection or malformed requests

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use `supabase.from('organization_configs').select().eq('organization_id', orgId).eq('flag_key', flagKey).maybeSingle()` for `getFlag` — `maybeSingle()` returns null on no match without throwing, which maps cleanly to `false`. For `getAllFlags`, use `.select('flag_key, flag_value').eq('organization_id', orgId)` and convert the list to a Map. For `setFlag`, use `.upsert({'organization_id': orgId, 'flag_key': flagKey, 'flag_value': value}, onConflict: 'organization_id,flag_key')`. Define `FeatureFlagRepositoryException` as a domain exception class, not re-throwing raw Supabase exceptions, so the caller is decoupled from the Supabase SDK.

Register the repository with Riverpod as a `Provider` so it can be overridden in tests. The interface pattern (`IFeatureFlagRepository`) is essential because the cache integration layer (task-007) will decorate the repository.

Testing Requirements

Unit tests using `mocktail` to mock `SupabaseClient` and `SupabaseQueryBuilder`. Test cases: (1) `getFlag` returns true when Supabase returns a matching enabled row; (2) `getFlag` returns false when no row found (empty result); (3) `getFlag` throws `FeatureFlagRepositoryException` on `PostgrestException`; (4) `getAllFlags` returns correct map from multi-row response; (5) `setFlag` calls upsert with correct payload including orgId and flagKey; (6) `setFlag` throws on network error. Integration tests against local Supabase instance: verify RLS blocks cross-org access at the repository level. Minimum 90% branch coverage on the repository class.

Run via `flutter test`.

Component
Feature Flag Repository
data medium
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.