high priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

A private _checkVersion(OrganizationId orgId) method (or equivalent) exists in the TerminologySyncService implementation
The method reads the cached updatedAt from TerminologyCacheAdapter without any network call
When no cached updatedAt exists (cold start), the method returns SyncDecision.refreshRequired with the server's updatedAt
The Supabase query fetches only the updated_at column from the terminology table for the given orgId — no full map data is transferred
The Supabase query uses a SELECT with a WHERE orgId clause and a LIMIT 1 to minimize response size
Comparison is strict: if server updated_at > cached updatedAt by any amount, returns refreshRequired; otherwise returns upToDate
The full version-check round-trip (cache read + network) must complete in under 500 ms on a standard 4G connection in production profiling
If the Supabase query fails, the method throws a typed TerminologySyncException (not a raw PostgrestException) so the caller can apply retry logic
Unit tests cover: cache miss → refreshRequired, cached == server → upToDate, cached < server → refreshRequired, Supabase error → TerminologySyncException

Technical Requirements

frameworks
Flutter
Supabase Flutter SDK
dart:async
apis
Supabase: SELECT updated_at FROM organization_terminology WHERE org_id = :orgId LIMIT 1
TerminologyCacheAdapter.readUpdatedAt(orgId)
data models
OrganizationId
SyncDecision
TerminologySyncException
DateTime (UTC)
performance requirements
Supabase query payload must be under 200 bytes (single timestamp column only)
Version check must not block the Flutter UI thread — must be called from an isolate-safe async context
Cache read must be synchronous or complete in under 5 ms
security requirements
The Supabase query must be executed with RLS enforced — the authenticated user must only be able to read their own organization's updated_at
Do not expose raw Supabase error messages to higher layers; wrap in TerminologySyncException with a sanitized message

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement as a private method _checkVersion on the concrete TerminologySyncServiceImpl class. Use Supabase's .from('organization_terminology').select('updated_at').eq('org_id', orgId.value).maybeSingle() to get a nullable result. Treat a null result the same as a cache miss (refreshRequired). Parse the returned updated_at string to DateTime.parse(...).toUtc() for comparison.

Store and compare all timestamps in UTC to avoid DST edge cases. Wrap the entire Supabase call in try/catch; map PostgrestException and SocketException to TerminologySyncException with a bool isNetworkError flag so the retry logic can distinguish permanent from transient failures. Keep this method pure and stateless — no side effects — so it can be unit tested in isolation.

Testing Requirements

Unit tests using flutter_test with mocked Supabase client (via mockito or manual stub). Test all four decision paths: (1) no cache entry → refreshRequired; (2) cache.updatedAt == server.updated_at → upToDate; (3) server.updated_at is 1 ms newer → refreshRequired; (4) Supabase throws → TerminologySyncException propagated. Verify the Supabase query string contains only 'updated_at' column and a WHERE clause (using argument capture on the mock). Add a performance smoke test that asserts mock-backed call completes in under 10 ms.

Component
Terminology Sync Service
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.