critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

TenantContextService.load() applies all feature flags from the selected org via FeatureFlagProvider before returning success
org_id is persisted to TenantSessionStore using flutter_secure_storage (iOS Keychain / Android Keystore) — never plain SharedPreferences
currentTenant exposes a Stream<TenantState> (or equivalent Riverpod provider) that emits the active tenant immediately after load() completes
All downstream widgets/BLoCs consuming currentTenant receive the updated state within one frame after load() resolves
If secure storage write fails, TenantContextService surfaces a typed StoragePersistenceException and does NOT emit a success state
If FeatureFlagProvider.apply() throws, TenantContextService rolls back and emits an error state — org_id is not persisted in this case
On app cold start, TenantContextService reads the persisted org_id from TenantSessionStore and re-hydrates currentTenant without requiring re-selection
Calling load() while already loading is a no-op (idempotent guard prevents duplicate seeding)
TenantContextService.clear() removes org_id from secure storage and resets currentTenant to TenantState.none

Technical Requirements

frameworks
Flutter
Riverpod
flutter_secure_storage
apis
Supabase Auth (JWT claims for org_id validation)
Supabase Edge Functions (feature flag fetch endpoint)
data models
assignment
accessibility_preferences
performance requirements
Secure storage write must complete within 200ms on mid-range devices
currentTenant stream must emit within 50ms of load() completing
Feature flag application must not block the UI thread — run in an isolate or as async microtask
security requirements
org_id stored only in flutter_secure_storage (iOS Keychain / Android Keystore)
Feature flags fetched via authenticated Supabase Edge Function — service role key never in mobile binary
currentTenant stream must not expose raw JWT or credentials
TenantSessionStore must encrypt the org_id value at rest
Clear() must perform a secure wipe — not just a null assignment

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Model TenantState as a sealed class or freezed union: TenantState.loading / TenantState.active(orgId, featureFlags) / TenantState.none / TenantState.error(exception). Use a StreamController.broadcast() or Riverpod AsyncNotifier for currentTenant. The load() method must follow a strict sequence: (1) validate org, (2) seed labels, (3) apply branding, (4) apply feature flags, (5) persist org_id — any step failure must not advance to step 5. Use a Mutex or similar guard (package:synchronized) to prevent re-entrant load() calls.

For TenantSessionStore, wrap flutter_secure_storage with a typed interface so tests can inject a fake. Feature flags should be cached in memory after application — do not re-fetch on every navigation event. Document the currentTenant stream contract clearly: null emission means 'loading', never 'no tenant'.

Testing Requirements

Unit tests: TenantContextService with mocked FeatureFlagProvider and TenantSessionStore — verify flag application order, persistence call sequencing, and rollback on partial failure. Test idempotency of load() under concurrent calls. Integration tests: verify org_id round-trips correctly through flutter_secure_storage on both iOS simulator (Keychain) and Android emulator (Keystore). Widget tests: verify downstream Riverpod consumers receive updated TenantState after load().

Edge case tests: storage write failure → error state emitted, no org_id persisted; FeatureFlagProvider throws → rollback, no persistence; cold start re-hydration from persisted org_id. Target 90%+ branch coverage on TenantContextService.

Component
Tenant Context Service
service high
Epic Risks (4)
high impact medium prob technical

TenantContextService must invalidate all downstream Riverpod providers when the org context changes (org switch scenario). If any provider caches org-specific data without subscribing to the tenant context, it will serve stale data from the previous org after a switch — which is both a UX failure and a potential GDPR violation.

Mitigation & Contingency

Mitigation: Define a single TenantContextProvider at the root of the Riverpod provider graph that all org-scoped providers depend on via ref.watch(). When TenantContextService.seedContext() runs, it invalidates TenantContextProvider which cascades invalidation to all dependents. Document this pattern in an architectural decision record so all developers follow it.

Contingency: Implement a post-switch integrity check that re-fetches a sample of each major data entity type and confirms the returned org_id matches the newly selected context; surface a reload prompt if any mismatch is detected.

medium impact medium prob security

MultiOrgMembershipResolver must query role assignments across potentially multiple tenant schemas. The anon or authenticated Supabase RLS policy may not permit cross-schema queries, making it impossible to return the full list of orgs a user belongs to in a single call.

Mitigation & Contingency

Mitigation: Design the membership query to use a dedicated Supabase edge function or a shared public schema view that aggregates role assignments across tenant schemas with a service-role key, returning only the org IDs the calling user is permitted to see. This keeps the client read-only.

Contingency: If cross-schema queries cannot be made safely, fall back to a per-org sequential membership check using the list of known org IDs and coalesce results client-side with appropriate timeout handling.

medium impact low prob technical

go_router redirect guards behave differently on web vs. mobile for deep links and browser back-button navigation. If the app is later deployed as a Progressive Web App (PWA) for admin use, the OrgRouteGuard may loop or fail to apply correctly on browser navigation events.

Mitigation & Contingency

Mitigation: Implement the guard as a GoRouter.redirect callback (not a ShellRoute redirect) following go_router best practices for platform-agnostic guards. Write widget tests that simulate navigation with and without auth/org context on both mobile and web target platforms in CI.

Contingency: If web-specific guard behaviour differs unacceptably, introduce a platform check in the guard and apply separate redirect logic branches for web vs. mobile until a unified solution is found.

medium impact medium prob scope

In Phase 2 the OrgSelectionService will need to coordinate the handoff to BankID/Vipps authentication after the org is selected, storing the returned personnummer against the correct tenant's member record. If the service is designed too narrowly for Phase 1 email/password flow, retrofitting Phase 2 will require invasive changes to an already-tested component.

Mitigation & Contingency

Mitigation: Design OrgSelectionService with an AuthHandoffStrategy interface from the start (Phase 1 implementation: email/password, Phase 2: BankID/Vipps). The strategy pattern makes the Phase 2 swap an additive change rather than a rewrite. Stub the interface in Phase 1 with a TODO comment referencing the Phase 2 epic.

Contingency: If Phase 2 requirements diverge significantly from Phase 1 assumptions, create a dedicated Phase2OrgSelectionService subclass that extends the base and overrides the auth handoff step, preserving Phase 1 behaviour unchanged.