critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

OrganizationLabelsNotifier subscribes to TenantContextService's org-change stream in its constructor or via a dedicated listen() initializer
When a new orgId is emitted, the notifier immediately transitions to TerminologyState.loading before any network call
The in-memory map is cleared (set to null/empty) at the start of each reload so label() returns key fallbacks during the transition, not stale data from the previous org
The new org's map is fetched from TerminologyRepository and written to TerminologyCacheAdapter using the new orgId
Any in-flight fetch for the previous org is cancelled (via CancelToken or equivalent) before the new fetch starts
If an org-switch event arrives while a retry timer is pending, the timer is cancelled and the reload starts immediately for the new org
State sequence on switch: ...loaded(oldMap) → loading() → loaded(newMap) — no intermediate loaded(oldMap) emission after the switch signal
The StreamSubscription to TenantContextService is cancelled in dispose() to prevent memory leaks
Unit tests confirm that stale map is not accessible via label() during transition (returns key fallback between switch signal and new load completion)

Technical Requirements

frameworks
Flutter
Riverpod
flutter_riverpod
dart:async
apis
TenantContextService.orgChanges (Stream<OrganizationId>)
TerminologyRepository.fetchMap(orgId)
TerminologyCacheAdapter.write(orgId, map, updatedAt)
TerminologyCacheAdapter.clear(orgId)
data models
OrganizationId
TerminologyState
TerminologyMap
performance requirements
State must reach loading() within one event-loop tick of receiving the org-switch signal
Old map must be unreachable via label() before the loading() state is emitted
security requirements
Never expose the previous organization's terminology map after an org-switch signal, even transiently
Ensure the new fetch is always scoped to the new orgId, not a captured closure over the old one

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use a StreamSubscription stored as an instance variable; initialize in the constructor after calling super(TerminologyState.loading()). Implement a private _loadForOrg(OrganizationId id) method that (1) sets _currentMap = null, (2) emits loading, (3) cancels any pending retry timer, (4) cancels the previous fetch CancelToken if Supabase supports it (otherwise ignore and check for stale result via a monotonic _fetchGeneration counter), (5) calls repository, (6) on success writes cache and emits loaded. The generation counter pattern is the safest race-condition guard in Dart: increment before each fetch, capture in local variable, discard result if local != current on return. Always null the in-memory map reference before emitting loading so label() falls back correctly during the window.

Testing Requirements

Unit tests with flutter_test and fake async. Use a StreamController as a mock TenantContextService. Simulate: (1) org switch while idle/loaded — verify loading state emitted before new loaded state; (2) org switch while fetch is in progress — verify previous fetch is abandoned and new fetch completes; (3) org switch while retry timer is active — verify timer cancelled and new fetch starts; (4) label() called during transition — verify key fallback returned. Use ProviderContainer with overrides to inject mocks.

Assert StreamSubscription is cancelled on container dispose.

Component
Organization Labels Notifier
service medium
Epic Risks (3)
high impact medium prob technical

When a user switches organization context (e.g., a coordinator with multi-org access), a race condition between the outgoing organization's map disposal and the incoming organization's fetch could briefly expose the wrong organization's terminology to the widget tree.

Mitigation & Contingency

Mitigation: Implement an explicit loading state in OrganizationLabelsNotifier that widgets check before rendering any resolved labels. The provider graph should cancel the previous organization's fetch via Riverpod's ref.onDispose before initiating the next.

Contingency: If the race manifests in production, fall back to English defaults during the transition window and emit a Sentry error event for investigation; the UX impact is a brief English flash rather than wrong-org terminology.

high impact low prob security

Supabase Row Level Security policies on organization_configs may inadvertently restrict the authenticated user from reading their own organization's labels JSONB column, causing silent empty maps that appear as English fallbacks.

Mitigation & Contingency

Mitigation: Write and test explicit RLS policies that grant SELECT on the labels column to any authenticated user whose organization_id matches. Add an integration test that verifies label fetch succeeds for each role (peer mentor, coordinator, admin).

Contingency: If RLS blocks are discovered in production, temporarily escalate label fetch to a service-role edge function while the RLS policy is corrected, ensuring no labels are exposed cross-organization.

medium impact medium prob scope

A peer mentor who installs the app for the first time with no internet connection will have no cached terminology map and will see only English defaults, which may be confusing for organizations like NHF that use Norwegian-specific role names exclusively.

Mitigation & Contingency

Mitigation: Bundle a default fallback terminology map for each known organization as a compile-time asset (Dart asset file) so that even fresh installs without connectivity render correct organizational terminology immediately.

Contingency: If bundled assets are out of date, display a one-time informational banner noting that terminology will update on next connectivity restore, with no functional blocking of the app.