critical priority medium complexity infrastructure pending backend specialist Tier 1

Acceptance Criteria

OrgFieldConfigLoader is a Dart class accepting SupabaseClient and ReportSchemaCache via constructor injection
loadConfig(String orgId, String featureKey, {bool forceRefresh = false}) → Future<FieldConfigSchema> is the primary public method
When forceRefresh is false and a valid (non-expired) cache entry exists, the cached FieldConfigSchema is returned without a Supabase query
When forceRefresh is true or cache miss/expired, a Supabase query is made to org_field_configs filtered by org_id and feature_key
On successful fetch, the JSONB config column is parsed into a FieldConfigSchema and stored in ReportSchemaCache before returning
When the org_field_configs table returns no rows for the given org_id + feature_key, a default FieldConfigSchema is returned (not an exception); the default schema includes the baseline fields: health_status, course_interest, assistive_device_situation, way_forward as visible text/select fields
Default fallback schema is defined as a constant and is not fetched from the network
Throws OrgFieldConfigLoaderException (typed) on network errors or unexpected Supabase errors — not on missing config (that uses fallback)
JSONB parsing handles missing optional fields in the config JSON gracefully (uses defaults for missing validation_rules, options, visible, etc.)
All FieldConfig objects in the returned schema are sorted by their displayOrder field ascending
forceRefresh=true invalidates the cache entry for the given orgId before fetching

Technical Requirements

frameworks
Flutter
Supabase (supabase_flutter)
apis
Supabase PostgREST REST API — org_field_configs table
data models
FieldConfigSchema
FieldConfig
FieldType
org_field_configs (Supabase table)
performance requirements
Cache hit must return synchronously from cache with no network round-trip
Supabase query must use .eq('org_id', orgId).eq('feature_key', featureKey).maybeSingle() — not a list fetch followed by client-side filter
JSONB parsing must not block the event loop — keep parsing synchronous but lightweight
security requirements
Loader relies on Supabase RLS for row-level access — do not pass org_id from client as a trusted source; RLS on the server enforces org membership
Do not cache configs across different authenticated users — invalidateAll() on logout
Validate that all parsed FieldType enum values are known; reject (log warning + skip field) if unknown field_type encountered to prevent future schema additions from crashing old app versions

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use .maybeSingle() rather than .single() when querying by org_id + feature_key, because the fallback path requires distinguishing 'no row found' from an error — .single() throws on zero rows while .maybeSingle() returns null. Parse the JSONB config column by calling jsonDecode on the returned string (Supabase Dart client may return it as a Map already — verify at runtime). Define the default fallback schema as a static const or static final on the class so it is clearly documented and easily updatable. Use the FieldConfig and FieldConfigSchema types defined in task-004's shared types file.

Document clearly that OrgFieldConfigLoader is scoped per feature_key — callers must pass 'post_session_report' as the featureKey when loading report field configs.

Testing Requirements

Write unit tests using flutter_test with mocked SupabaseClient and a real (non-mocked) ReportSchemaCache. Test: (1) cache hit returns cached schema without calling Supabase, (2) cache miss triggers Supabase query and caches result, (3) forceRefresh=true bypasses cache and re-fetches, (4) Supabase returns no rows → default fallback schema returned, (5) Supabase throws PostgrestException → OrgFieldConfigLoaderException thrown, (6) parsed schema fields are sorted by displayOrder, (7) missing optional JSON fields use safe defaults, (8) unknown FieldType value is skipped with warning and remaining fields still returned, (9) returned FieldConfigSchema.orgId matches the queried orgId. Write an integration test (if local Supabase available) that fetches seeded config and verifies round-trip JSON parsing.

Component
Organisation Field Config Loader
infrastructure medium
Epic Risks (3)
high impact medium prob security

Supabase RLS policies for multi-org report access may be more complex than anticipated — coordinators need cross-peer-mentor access within their org but not across orgs, and draft reports should be invisible to coordinators until submitted. Misconfigured RLS could expose sensitive health data or block legitimate access.

Mitigation & Contingency

Mitigation: Define and test RLS policies in isolation before writing repository code. Create a dedicated SQL migration file with policy definitions and an automated integration test suite that verifies each role's access boundaries using real Supabase auth tokens.

Contingency: If RLS proves too complex to express declaratively, implement application-level access control in the repository layer with explicit org and role checks, and add a security audit task before the feature goes to production.

high impact medium prob integration

The org field config JSON stored in Supabase may lack a stable, versioned schema contract. If different organisations have drifted to different field-definition formats, org-field-config-loader will fail silently or crash, breaking form rendering for those orgs.

Mitigation & Contingency

Mitigation: Define a canonical JSON Schema for field config and validate all existing org configs against it before implementation begins. Store a schema version field in every config record and handle version migrations explicitly in the loader.

Contingency: If existing configs are too heterogeneous, implement a config normalisation pass in org-field-config-loader that coerces known variants to the canonical format, logging warnings for fields that cannot be normalised so operations can fix them in the admin console.

medium impact low prob technical

TTL-based schema cache invalidation may cause peer mentors to use stale field definitions for up to the TTL window after an admin updates the org config, potentially collecting data against outdated field structures.

Mitigation & Contingency

Mitigation: Set a conservative TTL (e.g. 15 minutes) and expose a manual cache-bust mechanism triggered on app foreground-resume. Document the maximum staleness window in the admin console so org admins know to plan config changes outside active reporting windows.

Contingency: If stale schema causes a data quality incident, add a Supabase Realtime subscription to the org config table that invalidates the cache immediately on any config update.