Implement Supabase session custom claim write for RLS enforcement
epic-organization-selection-and-onboarding-data-layer-task-007 — Implement the Supabase session claim write path in TenantSessionStore. After persisting to SecureStorage, call the Supabase RPC function (or edge function) that writes the selected org_id as a custom JWT claim to the active session, enabling server-side RLS policies to immediately scope all subsequent queries to the selected tenant. Implement claim refresh logic to re-apply the claim after Supabase token refresh events. Handle RPC failure by rolling back the SecureStorage write to keep both persistence layers consistent.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
The server-side Postgres function `set_active_organization` should call `auth.jwt_claims_admin.set_claim(auth.uid(), 'active_organization_id', org_id::text)` or use the `supabase_jwt_claims` approach from the Supabase docs. On the Flutter side, after the RPC call, call `supabase.auth.refreshSession()` to obtain a new JWT containing the claim — Supabase does not automatically update the in-memory JWT after a custom claim write. Use a `Mutex` (from the `synchronized` package) around the claim-write path to prevent concurrent writes. For the rollback: wrap the entire persistSelection as `try { await secureStorage.write(...); await supabase.rpc(...); } catch (e) { await secureStorage.delete(...); rethrow; }`.
Keep the token refresh listener in a private `_authSubscription` cancelled in dispose().
Testing Requirements
Write unit tests with mocked Supabase RPC client: (1) happy path — SecureStorage write succeeds then RPC succeeds, no exception thrown, (2) RPC failure after SecureStorage write — assert SecureStorage entry is deleted (rollback) and TenantClaimWriteException is thrown, (3) token refresh event with stored session — assert RPC is called with correct org_id, (4) token refresh with no stored session — assert RPC is NOT called, (5) rapid double token refresh — assert RPC called exactly once (debounce). Write one integration test against a local Supabase instance (via `supabase start`) verifying the JWT contains the custom claim after the RPC call.
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.
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.