critical priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

ConcreteOrgRouteGuard implements the OrgRouteGuard contract defined in task-011 with no deviations
redirect() reads Supabase auth state via supabase.auth.currentSession (synchronous) — no async calls inside redirect
redirect() reads TenantContextService.currentTenantSync (synchronous last-value getter) — no stream.first or await inside redirect
Unauthenticated users navigating to any non-exempt route are redirected to '/login'
Authenticated users with TenantState.none navigating to any non-exempt route are redirected to '/org-selection'
Authenticated users with TenantState.loading return null (no redirect) to prevent flicker during cold start hydration
Authenticated users with TenantState.active pass through (return null) for all non-exempt routes
Navigating to /login or /org-selection while already authenticated with a valid tenant redirects to '/' (home) — prevents backward navigation to login after sign-in
Session expiry detected at navigation time redirects to '/login' and clears TenantContextService
Guard is registered as the top-level redirect in GoRouter configuration, verified by a router integration test
GDPR: no screen displaying org-specific data is reachable without TenantState.active — verified by attempting deep-link navigation in tests

Technical Requirements

frameworks
Flutter
go_router
Riverpod
Supabase Flutter SDK
apis
Supabase Auth (currentSession synchronous getter)
performance requirements
Guard evaluation synchronous — must not add measurable latency to navigation events
No heap allocations inside the hot redirect path — reuse cached state references
security requirements
GDPR compliance: all routes displaying personal data blocked without TenantState.active
Expired JWT must be detected (check session.expiresAt against DateTime.now()) and trigger login redirect
Guard must not be bypassable via deep links — go_router redirect runs for ALL navigation including external deep links
No sensitive state stored in the guard itself — always read from live sources
TenantContextService.clear() must be called before redirecting to login on session expiry to prevent stale tenant data

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

The key implementation challenge is the synchronous constraint of go_router's redirect. Implement currentTenantSync on TenantContextService as a getter that returns the last value emitted by the underlying StreamController (store it in a private _currentTenant field updated on every stream emission). This pattern is sometimes called a 'synchronous snapshot' of an async stream. For session expiry detection, compare supabase.auth.currentSession?.expiresAt (Unix timestamp) against DateTime.now().millisecondsSinceEpoch ~/ 1000.

Register the guard using GoRouter(redirect: (context, state) => ref.read(orgRouteGuardProvider).redirect(context, state)) — using Riverpod's ref.read inside the router redirect is safe because redirect is not called during widget builds. The reverse redirect (authenticated+active → /login) prevents the confusing UX of users seeing the login screen after successful sign-in if they press back. Use GoRouterState.matchedLocation for exempt route comparison to handle path parameters correctly.

Testing Requirements

Unit tests: inject mock auth session and mock TenantContextService into ConcreteOrgRouteGuard. Test all redirect matrix branches including the reverse redirects (authenticated+active tenant navigating to /login → redirect to home). Test expired session detection (set expiresAt to DateTime.now().subtract(Duration(minutes:1))). Widget/router integration tests: construct a GoRouter with the guard and verify navigation outcomes for each state combination using NavigationRailGoRouterProvider or equivalent test harness.

E2E test: simulate cold start with persisted org_id, verify guard returns null during loading and then null again once TenantState.active emits (no redirect to org-selection during hydration). GDPR compliance test: attempt programmatic deep-link to a protected route with TenantState.none — verify redirect to org-selection. Target 100% branch coverage on the redirect() function.

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.