critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

OrgRouteGuard is defined as a typed class (not a loose function) with a single public method: String? redirect(BuildContext context, GoRouterState state)
The redirect matrix is fully specified: (unauthenticated, any) → '/login', (authenticated, no tenant) → '/org-selection', (authenticated, tenant present) → null (allow through)
The loading state is explicitly handled: when TenantContextService is loading, redirect returns null to allow navigation to proceed without flicker
The guard's position in go_router configuration is documented — it must be the top-level redirect callback, not a per-route guard
Routes that are explicitly excluded from the guard (e.g., /login, /org-selection themselves) are enumerated to prevent redirect loops
The guard contract specifies that it reads auth and tenant state synchronously — it must never await within the redirect function
A unit-testable interface is defined so the guard can be tested with injected auth/tenant state without a full router setup
The guard's behavior during session expiry mid-session is documented: expired session → redirect to login regardless of tenant state

Technical Requirements

frameworks
Flutter
go_router
Riverpod
Supabase Auth
apis
Supabase Auth (session state)
performance requirements
Redirect evaluation must be synchronous — no async operations inside the redirect callback
Guard evaluation must complete in under 1ms — it runs on every navigation event
security requirements
Guard must block ALL routes (except login/org-selection) when tenant context is missing — GDPR compliance requires confirmed org context before any data is displayed
Session expiry must be detected and redirect to login — do not allow navigation with an expired JWT
Guard must not cache auth or tenant state internally — always read from the live Supabase session and TenantContextService stream

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

go_router's redirect callback must be synchronous — this is a hard framework constraint. This means OrgRouteGuard cannot call async methods. Design TenantContextService to expose a synchronous currentTenantSync getter (the last emitted value from the stream) alongside the async stream, so the guard can read it without awaiting. Similarly, use Supabase's supabase.auth.currentSession (synchronous) rather than any async session fetch.

The loading state (return null) prevents a flash of the org-selection screen during cold start re-hydration. Document the exemption list clearly — at minimum: /login, /org-selection, /no-access. Consider a const Set _guardExemptRoutes for readability. This task is definition-only; implementation is in task-012.

Testing Requirements

Unit tests for the redirect matrix covering all 6 meaningful state combinations: (unauthenticated, no tenant), (unauthenticated, tenant present — impossible but guard must still redirect to login), (authenticated, no tenant, not loading), (authenticated, no tenant, loading), (authenticated, tenant present), (expired session, tenant present). Verify that exempt routes (/login, /org-selection) do not trigger redirect loops. Verify that the guard returns null (not an empty string) for allow-through cases. No integration tests required at this stage — this is contract definition.

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.