critical priority medium complexity database pending database specialist Tier 2

Acceptance Criteria

getContactById (from task-003) is extended to additionally return a List<EncryptedFieldStub> containing one stub per encrypted field associated with the contact
Each EncryptedFieldStub contains: fieldName (String), keyReference (String — the Vault key id or key version identifier), lastReadAt (DateTime?, nullable), and isDecrypted (bool, always false at repository layer)
recordFieldReadReceipt(String contactId, String fieldName) updates the read-receipt timestamp for a field and is called by the decryption layer after successful decryption — it must not be called from the UI layer directly
getEncryptedFieldsForContact(String contactId) is available as a standalone method returning only the stub list for scenarios where the full contact is already cached
Stub list is returned even if the authenticated user lacks decryption permission — the UI decides what to show; the repository only surfaces metadata
Read-receipt timestamps are written with the server timestamp (Supabase now()) not the client clock
All metadata queries respect the same RLS policy as the contact row — cross-organisation metadata access is blocked at the database level
The encrypted field blob content is never fetched in these queries — only metadata columns (field_name, key_reference, read_at) are selected

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase PostgREST REST API
data models
EncryptedFieldStub
ContactDetailRecord (extended)
performance requirements
Encrypted field metadata must be fetched in the same query as the contact profile (task-003 JOIN) to avoid a second round-trip on initial load
recordFieldReadReceipt must complete as a fire-and-forget background call — it must not block the UI thread or the decryption flow
security requirements
Encrypted field blob columns must be explicitly excluded from the .select() list — never use wildcard '*' on tables containing encrypted data
keyReference values must not be logged in debug output — they are sensitive key identifiers
Read-receipt recording must use the server-side now() timestamp to prevent client clock manipulation

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Store encrypted field metadata in a separate table (e.g., contact_encrypted_field_meta) with columns: contact_id, field_name, key_reference, key_version, read_at. Join this table in the existing contact query using PostgREST embedded resource syntax. For the read-receipt update, use Supabase RPC (a database function) that accepts contact_id and field_name and sets read_at = now() — this guarantees server-side timestamping and avoids a client update with a user-controlled timestamp. Keep EncryptedFieldStub immutable (use Dart's @immutable annotation); the decryption layer creates a new DecryptedField object rather than mutating the stub.

This clean separation prevents accidental logging of decrypted data.

Testing Requirements

Unit tests: (1) getContactById returns ContactDetailRecord with a non-empty encryptedFieldStubs list when the contact has encrypted fields; (2) getEncryptedFieldsForContact returns only the stub list without full contact data; (3) recordFieldReadReceipt calls the correct Supabase update with server-timestamp instruction; (4) encrypted blob columns are absent from all query SELECT lists (assert on the query string or mock call arguments). Integration test: verify that a coordinator cannot read encrypted field metadata for a contact outside their organisation. Verify read-receipt timestamp is set to the server time after calling recordFieldReadReceipt.

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.