high priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

Service exposes `recordReceipt(contactId, fieldIdentifier)` that inserts a row into `field_read_receipts` with `user_id` (from Supabase auth), `contact_id`, `field_identifier`, and `read_at` timestamp
Duplicate receipt writes are idempotent — if a receipt already exists for the same (user_id, contact_id, field_identifier) triple, no duplicate row is created (upsert with conflict ignore)
Service exposes `watchReceiptStatus(contactId, fieldIdentifier)` returning a `Stream<ReceiptStatus>` (enum: `read` | `unread`) that emits immediately on subscription and updates if remote state changes
Supabase RLS policy enforces that a coordinator can only read their own receipt rows (auth.uid() = user_id); attempting to read another user's receipts returns no rows, not an error
Receipt records are never deleted; the service only inserts and reads
Service does not store or log the decrypted field value — only the field identifier string (e.g., 'name', 'address', 'epikrise_ref')
When Supabase is unreachable, `watchReceiptStatus` emits `unread` as a safe default and retries silently in the background
Unit tests cover: first-reveal triggers insert, second-reveal is idempotent, stream emits `read` after insert, RLS rejection surfaces as `unread` (not crash)

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase REST API (field_read_receipts table UPSERT + SELECT)
Supabase Realtime (optional — subscribe to own receipt rows for live updates)
data models
FieldReadReceipt (user_id, contact_id, field_identifier, read_at)
ReceiptStatus (enum: read / unread)
performance requirements
`recordReceipt()` must complete the Supabase upsert within 1.5s; failure must not block the UI
`watchReceiptStatus()` must emit initial state within 200ms of subscription
Service must handle Supabase Realtime disconnection gracefully with reconnect backoff
security requirements
Supabase RLS: INSERT policy requires auth.uid() = user_id; SELECT policy requires auth.uid() = user_id
Field identifier values must be validated against an allowlist (e.g., ['name','address','epikrise_ref']) before insert to prevent injection of arbitrary strings
No PII is stored in the receipt table — only identifiers and timestamps
Service must use field-encryption-utils token/key only for decryption trigger; receipt recording must happen after successful decryption, not before

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use Supabase's `upsert()` with `onConflict: 'user_id,contact_id,field_identifier'` and `ignoreDuplicates: true` to achieve idempotency at the database level. Define the `field_read_receipts` table with a composite unique constraint on these three columns. For `watchReceiptStatus()`, perform an initial SELECT on subscription, then optionally subscribe to Supabase Realtime changes filtered to the current user. If Realtime is not enabled, a simple periodic poll (every 30s) is acceptable for v1.

Keep the service stateless — do not cache receipt state in memory, as the source of truth is Supabase. Provide the service as a Riverpod `Provider` (not `StateNotifier`) since it has no mutable local state.

Testing Requirements

Unit tests (flutter_test): mock Supabase client; verify `recordReceipt()` performs upsert with correct payload; verify idempotent behaviour on duplicate call; verify `watchReceiptStatus()` emits `unread` before insert and `read` after. Integration test against local Supabase: verify RLS prevents reading receipts created by a different user ID. Test network failure scenario — service should not throw, should emit `unread`. Target ≥85% line coverage.

Component
Contact Detail Screen
ui medium
Epic Risks (2)
low impact medium prob dependency

The Peer Mentor Profile tab on the contact detail screen depends on the peer-mentor-detail-screen-widget being delivered by the separate Peer Mentor Detail feature. If that feature is delayed, the navigation affordance will be present but lead to a stub screen, which may confuse coordinators in the TestFlight pilot.

Mitigation & Contingency

Mitigation: Implement the peer mentor tab with a feature flag guard. When the Peer Mentor Detail feature is incomplete, the flag disables the tab. Coordinate delivery timelines with the team responsible for Peer Mentor Detail to align TestFlight releases.

Contingency: If the Peer Mentor Detail feature is significantly delayed, ship the contact detail screen without the peer mentor tab in the first TestFlight build and add it as an incremental update once the dependent screen is ready.

medium impact medium prob technical

The contact detail screen must adapt its layout significantly based on organisation context: NHF shows affiliation chips, Blindeforbundet shows encrypted fields and assignment status, standard contacts show neither. Managing this conditional rendering without introducing bugs in each variant is complex and increases the risk of organisation-specific regressions.

Mitigation & Contingency

Mitigation: Define a ContactDetailViewModel that resolves all org-specific flags (showEncryptedFields, showAssignmentStatus, showMultiChapterChips) from the organisation config before the widget tree renders. Widget tests must cover all three organisation variants as separate test cases to catch regressions.

Contingency: If conditional rendering logic grows unwieldy, refactor into separate composable section widgets (ProfileHeaderSection, AffiliationSection, EncryptedFieldsSection) that are conditionally included by the parent screen, isolating org-specific logic to individual components.