critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

persistSelection() serializes TenantSessionData to a JSON string and writes it to SecureStorageAdapter using key `tenant_session_{userId}` where userId is the current Supabase Auth UID
restoreSelection() reads the key `tenant_session_{userId}`, deserializes the JSON, and returns a valid TenantSessionData instance; returns null if the key is absent
restoreSelection() returns null (does not throw) if the stored JSON is malformed or fails TenantSessionData.fromJson() validation
clearSelection() deletes the key `tenant_session_{userId}` from SecureStorageAdapter
All three methods are tested against a mock SecureStorageAdapter that records read/write/delete calls
SecureStorage write failure (e.g., Keychain unavailable) surfaces as a typed SecureStoragePersistenceException rather than an unhandled platform exception
The userId used for the key is fetched from Supabase Auth at call time — not cached at construction time — so role switches within a session use the correct key
No TenantSessionData is ever written to SharedPreferences or any unencrypted store

Technical Requirements

frameworks
Flutter
flutter_secure_storage
flutter_test
apis
Supabase Auth (currentUser.id for key scoping)
data models
accessibility_preferences
performance requirements
SecureStorage read on iOS Keychain typically completes in < 50 ms; the implementation must not block the UI thread — always await in an async method
persistSelection() and restoreSelection() must not be called on the main isolate during heavy UI work; callers should invoke from a Riverpod async provider
security requirements
flutter_secure_storage stores data in iOS Keychain (kSecClassGenericPassword) and Android EncryptedSharedPreferences — verify the package configuration includes `iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock)`
Key scoped to userId prevents cross-user data leakage when device is shared or user switches accounts
JSON stored in Keychain must not contain raw JWTs, passwords, or national identity numbers
On iOS, set `synchronizable: false` in IOSOptions to prevent iCloud Keychain sync of session data

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Inject SecureStorageAdapter and a SupabaseAuthClient as constructor dependencies for testability — do not instantiate FlutterSecureStorage directly inside the class. Use a private helper `String _storageKey(String userId) => 'tenant_session_$userId'` for key generation. Wrap the FlutterSecureStorage write in a try/catch on PlatformException, rethrowing as a typed SecureStoragePersistenceException with a descriptive message. For the read path, use `jsonDecode` inside a try/catch to guard against corrupt data.

The `persistSelection` implementation for this task covers only the SecureStorage path; the Supabase claim path is added in task-007 as a second phase of the same method.

Testing Requirements

Write unit tests in flutter_test using a MockSecureStorageAdapter (Mockito or manual fake): (1) persistSelection writes correct key and JSON-serialized value, (2) restoreSelection reads correct key and returns deserialized TenantSessionData, (3) restoreSelection returns null when key not found, (4) restoreSelection returns null when stored JSON is corrupt, (5) clearSelection calls delete on the correct key, (6) verify key includes userId suffix by injecting a mock auth provider returning a known UID. No device-level integration tests required at this layer.

Component
Tenant Session Store
data medium
Epic Risks (2)
high impact low prob technical

TenantSessionStore must write to both SecureStorageAdapter and Supabase session synchronously. If the Supabase write succeeds but the local secure storage write fails (or vice versa), the system ends up in an inconsistent state: local app thinks org A is selected but Supabase queries are scoped to org B (or unscoped), causing silent data leakage or empty result sets.

Mitigation & Contingency

Mitigation: Implement the dual-write with compensating rollback: if the second write fails, undo the first write and surface a typed DualWriteFailureError to callers. Add a startup integrity check in restoreSession() that validates local storage and Supabase session agree on the current org_id.

Contingency: If integrity check fails on startup, clear both stores and redirect the user to the org selection screen rather than proceeding with potentially mismatched state.

medium impact low prob integration

An organization could be deactivated in Supabase between the time the org list is cached and the time the user taps to select it. If the repository serves stale cached data the org-selection-service will attempt to seed a context for an inactive org, potentially causing RLS scope issues or confusing error states.

Mitigation & Contingency

Mitigation: The OrganizationRepository.getOrganizationById() path used during selection validation always performs a fresh network fetch (bypassing cache) to confirm the org is still active before the TenantSessionStore writes anything.

Contingency: If a freshness check finds the org is inactive, surface a localized error message on the selection screen ('This organization is no longer available') and refresh the org list to show only currently active partners.