critical priority medium complexity database pending database specialist Tier 1

Acceptance Criteria

getContactById(String id) returns a ContactDetailRecord containing all non-encrypted plaintext columns or throws ContactNotFoundException if no matching row exists within the caller's RLS scope
ContactDetailRecord includes a chapterAffiliations list (List<ChapterAffiliation>) populated via a JOIN or sub-select on contact_chapters — minimum fields: chapterId, chapterName, isPrimary
A contact belonging to multiple chapters (up to 5 per NHF rules) returns all affiliations in a single query — no N+1 pattern
Encrypted fields are returned as EncryptedFieldStub objects (fieldName, keyReference, lastReadAt) and are not mixed into the plaintext ContactDetailRecord to keep the type boundary clear
RLS policy on the contacts table restricts coordinators to contacts within their own organisation; the repository must not add a client-side organisation filter as a substitute
getContactById called with an id belonging to a different organisation returns ContactNotFoundException (same as not found — no information leakage about existence)
Repository exposes a Stream<ContactDetailRecord> watchContactById(String id) method backed by Supabase Realtime for live updates on the detail screen
All database column names are referenced via a SupabaseColumnConstants class or equivalent constant file — no bare string literals in query builders
Repository is registered as a Riverpod Provider and is mockable for BLoC unit tests

Technical Requirements

frameworks
Flutter
Riverpod
BLoC
apis
Supabase PostgREST REST API
Supabase Realtime
data models
ContactDetailRecord
ChapterAffiliation
EncryptedFieldStub
ContactNotFoundException
performance requirements
getContactById must return within 1.5 seconds including chapter join on a standard mobile connection
Supabase .select() column list must be explicit (not '*') to avoid over-fetching encrypted blobs
Chapter affiliations must be fetched in the same query as the contact using PostgREST embedded resource syntax (contact_chapters(*)) to avoid N+1
security requirements
RLS policy is the authoritative access control — client-side org filtering is an optimisation only, never a security boundary
Encrypted field blobs must never be logged, printed, or included in crash reports — EncryptedFieldStub contains only metadata
ContactNotFoundException must be returned for both 'not found' and 'access denied' cases to prevent enumeration attacks

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use PostgREST embedded resource syntax in the .select() call: `contacts(*, contact_chapters(chapter_id, chapter_name, is_primary))` to get contact + chapters in one round-trip. Map the nested JSON array directly to List in fromJson. Identify encrypted columns by a naming convention (e.g., columns suffixed `_enc`) or a separate metadata table queried in task-004 — keep the detection logic consistent between both tasks. For Realtime, subscribe to the contacts channel filtered by `id=eq.{contactId}` and map the payload to ContactDetailRecord using the same fromJson factory.

Define all Supabase table and column names as static const strings in a SupabaseSchema class to avoid typos that cause silent runtime failures.

Testing Requirements

Unit tests with a mocked Supabase client: (1) getContactById returns ContactDetailRecord with correctly mapped plaintext fields; (2) chapterAffiliations list contains all chapters from the mocked join result; (3) ContactNotFoundException is thrown when Supabase returns an empty list; (4) EncryptedFieldStub objects are present and plaintext fields are absent for encrypted columns. Integration test against a local Supabase instance: verify RLS blocks cross-organisation reads, verify multi-chapter contact returns all affiliations. Test watchContactById emits updated records when the contact row is modified via Realtime.

Component
Contact Detail Repository
data medium
Epic Risks (3)
high impact medium prob security

Blindeforbundet's encryption key retrieval mechanism may not be finalised at implementation time, or session key availability via Supabase RLS may be inconsistent, causing decryption failures that expose masked placeholders to users and degrade the experience.

Mitigation & Contingency

Mitigation: Agree with Blindeforbundet on key storage and retrieval contract before implementation starts. Prototype key retrieval in a spike against the staging Supabase instance and validate the full decrypt/verify cycle with real test data before committing to the implementation.

Contingency: Implement a fallback that shows a 'field temporarily unavailable' state with a retry affordance. Log decryption failures server-side for audit. Escalate to Blindeforbundet stakeholders to unblock key management before the service tier epic begins.

medium impact medium prob technical

NHF contacts may belong to up to 5 chapters, each governed by separate RLS policies. A coordinator's chapter scope may not cover all affiliations, causing partial profile reads or silent data omissions that are difficult to detect in tests.

Mitigation & Contingency

Mitigation: Map all RLS policy combinations for multi-chapter contacts early. Write integration tests that create contacts with 5 affiliations and query them from coordinators with varying chapter scopes. Use Supabase's RLS test utilities to verify row visibility per role.

Contingency: Add an explicit 'affiliation partially visible' state in the repository response model so the UI can communicate scope limitations to the coordinator rather than silently showing incomplete data.

low impact medium prob scope

Organisation-specific validation rules (e.g., NHF chapter limit, Blindeforbundet encrypted field edit flow) may expand in scope during implementation as edge cases are discovered, causing the validator to grow beyond the planned complexity.

Mitigation & Contingency

Mitigation: Define the complete validation rule set with product and org stakeholders before coding begins. Document each rule with its source organisation and acceptance test. Use a rule registry pattern so new rules can be added without modifying core validator logic.

Contingency: Timebox validator enhancements to 2 hours per additional rule. Defer non-blocking rules to a follow-on maintenance task rather than blocking the epic delivery.