high priority medium complexity backend pending backend specialist Tier 5

Acceptance Criteria

On cold start, if TenantSessionStore contains a persisted org_id, TenantContextService.restore() is called and completes before the router resolves the first navigation event
The org-selection screen does NOT flash briefly before the main app screen on cold start with a valid persisted session
If Supabase auth session is expired or invalid, the tenant restore is skipped and the user is directed to the login screen (not org-selection)
If TenantSessionStore has a persisted org_id but the org is now deactivated, restore fails gracefully with a typed OrgDeactivatedError and navigates to an error screen
Auth restore and tenant restore are treated as a single atomic operation: either both succeed or neither is applied to the navigation state
If TenantContextService.restore() throws any error, the guard falls through to the org-selection screen rather than crashing the app
A loading/splash state is shown during the restore sequence — no blank screen or flicker
The guard correctly handles the race condition where Supabase session restore is still in-flight when the first route is evaluated
OrgRouteGuard does not block navigation for unauthenticated routes (login, splash)
Restore sequence completes within 2 seconds on a normal network connection; shows a timeout error after 10 seconds

Technical Requirements

frameworks
Flutter
Riverpod (for guard state observation)
BLoC (if guard delegates to a Cubit/BLoC)
apis
Supabase Auth (session restore via supabase.auth.currentSession)
Supabase Auth onAuthStateChange stream
TenantContextService.restore(orgId)
TenantSessionStore.getPersistedOrgId()
data models
assignment (to verify active membership for persisted org)
contact (authenticated user identity cross-reference)
performance requirements
Cold start restore sequence must not add more than 500ms to perceived app launch time
Restore must be gated behind a Future that completes before RouterDelegate resolves the initial location
No redundant Supabase queries during restore — use cached session where available
security requirements
JWTs stored in flutter_secure_storage (iOS Keychain / Android Keystore) — never plain SharedPreferences
Supabase session validation must happen server-side via JWT verification, not client-side only
Persisted org_id must be validated against the authenticated user's actual memberships on restore — prevent org_id spoofing
RLS policies enforced automatically via Supabase client for all membership queries during restore
ui components
Splash/loading overlay shown during restore sequence
Typed error screen for OrgDeactivatedError on restore failure

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Use a FutureProvider or AsyncNotifier (Riverpod) to model the cold-start restore state so the RouterDelegate can await it before resolving routes. The canonical pattern is to have the router's redirect callback return null (no redirect) until the restore Future completes, then evaluate the result. Store the restore result in a sealed class (RestoreSuccess | RestoreRequiresOrgSelection | RestoreError) to make the guard logic exhaustive and compiler-checked. Avoid using a boolean flag — use a typed state machine.

Coordinate with the Supabase session manager by listening to `supabase.auth.onAuthStateChange` and only triggering tenant restore after an INITIAL_SESSION or SIGNED_IN event, never on TOKEN_REFRESHED (to avoid re-triggering restore mid-session). Use flutter_secure_storage for all persisted org_id storage — never SharedPreferences. Be careful about the splash screen: use a native splash that stays visible until the restore Future resolves to prevent any white-frame flash on iOS.

Testing Requirements

Unit tests: mock TenantSessionStore to return a persisted org_id and verify OrgRouteGuard calls TenantContextService.restore() before allowing navigation; mock restore throwing each error type and verify correct fallback navigation target. Integration tests: simulate cold start with valid session, expired session, and deactivated org session — assert correct screen is displayed without any intermediate flash. Use flutter_test with mocktail for all Supabase and service mocks. Test the race condition by artificially delaying Supabase session restore and verifying the guard waits correctly.

Verify no regression on the unauthenticated login route (guard must not block it). Minimum 90% branch coverage on guard logic.

Component
Organization Route Guard
service medium
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.