critical priority low complexity infrastructure pending infrastructure specialist Tier 0

Acceptance Criteria

SecureStorageAdapter is an abstract class (or Dart interface via abstract class) with no concrete implementation details
Keys are expressed as a sealed class or enum (e.g., SecureStorageKey) — no raw strings passed to read/write
read() returns Future<String?> — null when key not found, never throws for missing entries
write() returns Future<void> and throws SecureStorageEncryptionException on encryption failure
delete() is idempotent — calling it on a non-existent key does not throw
exists() returns Future<bool> without reading the value
A SecureStorageException sealed class hierarchy covers: SecureStorageEncryptionException, SecureStoragePlatformUnavailableException, and SecureStorageUnknownException
The interface is located in a dedicated file (e.g., secure_storage_adapter.dart) under the appropriate layer (data/adapters or infrastructure)
The interface has no dependency on flutter_secure_storage — it is a pure Dart contract
Code review approved with no raw dynamic types in method signatures

Technical Requirements

frameworks
Flutter
Dart (sealed classes / abstract classes)
data models
SecureStorageKey (enum/sealed)
SecureStorageException (sealed hierarchy)
TenantSessionData (consumer — not defined here)
performance requirements
Interface adds zero runtime overhead — pure abstract contract
security requirements
Keys must be strongly typed to prevent accidental use of arbitrary strings as storage keys
Exception types must not leak platform-specific error messages to calling code — wrap and translate
Interface must not expose any method that returns raw bytes or platform handles

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Prefer a Dart sealed class for SecureStorageKey over an enum if you anticipate keys needing metadata (e.g., a human-readable label for debugging). If keys are simple identifiers with no extra data, a plain enum is cleaner. The exception hierarchy should use Dart sealed classes introduced in Dart 3 so callers can exhaustively switch on error types. Place the interface in `lib/data/adapters/secure_storage_adapter.dart` and the key definitions in the same file.

Avoid splitting the contract across multiple files at this stage — it is a small, cohesive unit. The FakeSecureStorageAdapter for tests belongs in `test/fakes/fake_secure_storage_adapter.dart`. Do not add any import from flutter_secure_storage in this file — that coupling belongs in the concrete implementation task.

Testing Requirements

No runtime tests required for a pure interface definition. However, a FakeSecureStorageAdapter (implements SecureStorageAdapter, backed by Map) must be implemented alongside the interface in a test support file.

This fake is used by all unit tests across the epic. Verify the fake itself with a trivial test confirming write-then-read, delete, and exists semantics match the contract.

Component
Secure Storage Adapter
infrastructure low
Epic Risks (3)
high impact medium prob technical

iOS Keychain and Android Keystore have meaningfully different failure modes and permission models. The secure storage plugin may throw platform-specific exceptions (e.g., biometric enrollment required, Keystore wipe after device re-enrolment) that crash higher-level flows if not caught at the adapter boundary.

Mitigation & Contingency

Mitigation: Wrap all storage plugin calls in try/catch at the adapter layer and expose a typed StorageResult<T> instead of throwing. Write integration tests on real device simulators for both platforms in CI using Fastlane. Document the exception matrix during spike.

Contingency: If a platform-specific failure cannot be handled gracefully, fall back to in-memory-only storage for the current session and surface a non-blocking warning to the user; log the event for investigation.

high impact medium prob integration

Setting a session-level Postgres variable (app.current_org_id) via a Supabase RPC requires that RLS policies on every table reference this variable. If the Supabase project schema has not yet defined these policies, the configurator will set the variable but queries will return unfiltered data, giving a false sense of security.

Mitigation & Contingency

Mitigation: Include a smoke-test RPC in the SupabaseRLSTenantConfigurator that verifies the variable is readable from a policy-scoped query before marking setup as complete. Coordinate with the database migration task to ensure RLS policies reference app.current_org_id before the configurator is shipped.

Contingency: If RLS policies are not in place at integration time, gate all data-fetching components behind a runtime check in SupabaseRLSTenantConfigurator.isRlsScopeVerified(); block data access and surface a developer warning until policies are confirmed.

medium impact medium prob technical

Fetching feature flags from Supabase on every cold start adds network latency before the first branded screen renders. On slow connections this may cause a perceptible blank-screen gap or cause the app to render with default (unflagged) state before flags arrive.

Mitigation & Contingency

Mitigation: Persist the last-known flag set to disk in the FeatureFlagProvider and serve stale-while-revalidate on startup. Gate flag refresh behind a configurable TTL (default 15 minutes) so network calls are not made on every launch.

Contingency: If stale flags cause a feature to appear that should be hidden, add a post-load re-evaluation pass that reconciles the live flag set with the rendered widget tree and triggers a targeted rebuild where needed.