critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

The `load(orgId)` method in `SupabaseTenantContextService` is extended to fetch branding config from `OrgBrandingCache` after terminology labels are applied
Branding config is fetched from Supabase (e.g., `org_branding` table) and cached in `OrgBrandingCache` keyed by orgId to prevent re-fetching on subsequent navigations
`BrandingTokens` (primaryColor, logoAssetPath, fontVariant) are applied to the app's ThemeData by calling a `ThemeNotifier.applyBranding(BrandingTokens)` method
Theme update is atomic: `ThemeNotifier.applyBranding` sets the new `ThemeData` in a single state emission — no intermediate partially-applied state is observable
No visible flash or flicker occurs between the org selection screen and the home screen: the theme is fully applied before the route transition completes (verified by integration test or manual QA)
If no branding config is found for the org, the app falls back to the default theme without error
TenantContext state transitions correctly: after branding is applied, status remains `ready` (not reset to `loading`)
Cache invalidation: `OrgBrandingCache` is cleared on sign-out (consistent with `InMemoryMembershipCache` pattern from task-003)
WCAG 2.2 AA contrast is maintained — if org's primaryColor does not meet AA contrast ratio against white, the system logs a warning and falls back to the default primary color
Dart analyzer reports zero errors; Flutter debug mode shows zero theme-related assertion errors

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase PostgREST API
data models
TenantContext
BrandingTokens
OrgBrandingCache
performance requirements
Branding fetch + theme application must complete within 500ms from cache hit or 2 seconds from Supabase on 4G
ThemeData construction must not block the main thread — pre-compute ColorScheme on a background isolate if necessary
OrgBrandingCache hit must be synchronous — no async overhead for repeated navigations
security requirements
Logo asset paths from Supabase must be validated against an allowlist of trusted origins before use in Image.network()
primaryColor hex strings from Supabase must be sanitized and validated before parsing to Color to prevent malformed values causing app crash
Branding config must only be fetchable for orgs the authenticated user belongs to (RLS enforced on org_branding table)
ui components
ThemeNotifier
MaterialApp theme consumer

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

The theme application must be a single atomic operation to avoid flicker. The recommended pattern is: construct the full new `ThemeData` object in memory first, then emit it as a single Riverpod state update — never update `primaryColor` and `textTheme` in separate notifier calls. Use Flutter's `ColorScheme.fromSeed(seedColor: tokens.primaryColor)` to generate a full Material 3 color scheme from the org's primary color — this ensures all derived colors (secondary, surface, onPrimary, etc.) are coherent and accessible. For WCAG contrast validation, implement a simple helper `bool meetsAA(Color foreground, Color background)` using the W3C relative luminance formula — this is a ~10-line utility function.

The `load` sequence in `SupabaseTenantContextService` after this task: (1) emit loading, (2) fetch labels → inject labels, (3) fetch branding → apply theme, (4) emit ready. Both steps 2 and 3 must complete before emitting `ready` to prevent partially-initialized tenant context reaching the UI.

Testing Requirements

Unit tests with `flutter_test`: (1) valid hex color string parses correctly to Flutter `Color`, (2) invalid hex string falls back to default primary color and logs a warning, (3) `OrgBrandingCache.get(orgId)` returns null on first call, the resolver fetches from Supabase, and a second call returns the cached value without a Supabase call, (4) `ThemeNotifier.applyBranding(tokens)` emits a single state update with the updated ThemeData — assert `notifyListeners` called exactly once, (5) org with no branding entry results in default theme being preserved. Widget integration test: wrap a `MaterialApp` with a `ThemeNotifier`-driven theme, call `applyBranding`, pump one frame, and assert the rendered primary color matches the injected value. WCAG contrast check: write a test that asserts a known low-contrast color triggers the fallback path.

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.