critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

SupabaseRLSTenantConfigurator exposes setActiveTenant(String orgId) and clearActiveTenant() methods
setActiveTenant() calls the Supabase RPC (e.g., set_current_org_id) and on success writes the orgId to SecureStorage via SecureStorageAdapter
If the RPC call fails, SecureStorage is not written (write is conditional on RPC success)
clearActiveTenant() calls the RPC to remove the claim and deletes the SecureStorage entry
restoreActiveTenant() reads from SecureStorage and, if a value exists, calls the RPC to re-apply the claim
restoreActiveTenant() returns the restored TenantSessionData or null if no session was persisted
After setActiveTenant(), any Supabase query filtered by RLS using app.current_org_id returns only that tenant's rows
A Riverpod Provider<SupabaseRLSTenantConfigurator> is exposed for DI
The service has no UI dependencies — it is a pure service class
All public methods are documented with dart doc comments explaining their side effects

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Flutter SDK
apis
Supabase RPC (set_current_org_id / clear_current_org_id)
Supabase Auth (session/JWT access)
data models
TenantSessionData
SecureStorageKey (org_session key)
performance requirements
setActiveTenant() must complete within 2 seconds on a normal mobile connection
restoreActiveTenant() is called on app startup and must not block the splash screen for more than 1 second
security requirements
The org ID must be set server-side via RPC — the claim must never be constructed client-side and injected into the JWT
The Supabase RPC must validate that the calling user is actually a member of the requested org before setting the claim
SecureStorage entry must be cleared on sign-out to prevent stale claim restoration after re-authentication as a different user
Org ID in SecureStorage must be treated as untrusted input when passed to the RPC — the server enforces membership, not the client

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

The RPC function on the Supabase side (PostgreSQL) should look like: `CREATE FUNCTION set_current_org_id(org_id uuid) RETURNS void AS $$ BEGIN IF NOT EXISTS (SELECT 1 FROM memberships WHERE user_id = auth.uid() AND org_id = $1) THEN RAISE EXCEPTION 'unauthorized'; END IF; PERFORM set_config('app.current_org_id', $1::text, false); END; $$ LANGUAGE plpgsql SECURITY DEFINER;` — the Flutter side just calls `supabase.rpc('set_current_org_id', params: {'org_id': orgId})`. On the Flutter side, inject SupabaseClient and SecureStorageAdapter via constructor (not via static globals) to keep the service testable. Use a Riverpod `Provider` (not `StateProvider`) since the configurator is stateless — it delegates state to Supabase session and SecureStorage. The restoreActiveTenant() method should be called from the app's initialization sequence (e.g., in main() after Supabase.initialize()) before routing to any authenticated screen.

Testing Requirements

Unit tests (mocktail): mock SecureStorageAdapter and Supabase RPC client. Test setActiveTenant() success path (RPC succeeds → write called), RPC failure path (RPC throws → write NOT called, exception propagated), clearActiveTenant() (RPC called + delete called), and restoreActiveTenant() with and without a persisted value. Integration tests (test Supabase project): call setActiveTenant() with a valid org, execute a query against an RLS-protected table, and assert only that tenant's rows are returned. Call clearActiveTenant() and assert the same query returns no rows or an unauthorized error.

Test restart simulation by calling restoreActiveTenant() on a fresh service instance with a seeded SecureStorage. Verify the RPC on the Supabase side rejects membership claims for orgs the user does not belong to.

Component
Supabase RLS Tenant Scope Configurator
infrastructure medium
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.