critical priority medium complexity database pending database specialist Tier 1

Acceptance Criteria

SupabaseContactChapterRepository implements ContactChapterRepository and can be registered as its implementation in the Riverpod provider
getChaptersForContact queries the contact_chapter_affiliations table filtered by contact_id and returns a List<ChapterAffiliation> mapped to domain models
addChapterAffiliation first counts existing affiliations for the contact; if count >= 5 it throws MaxAffiliationsExceededException with the current count
addChapterAffiliation checks for an existing row with the same contact_id + chapter_id; if found, throws AlreadyAssignedToChapterException before attempting insert
addChapterAffiliation inserts the new row atomically and returns the created ChapterAffiliation with a server-generated id and affiliatedAt timestamp
removeChapterAffiliation deletes the row identified by affiliationId; if no row is found, throws AffiliationNotFoundException
updateAffiliation updates the isPrimary field for the given affiliationId and returns the updated ChapterAffiliation
All Supabase errors are caught and either mapped to domain exceptions or rethrown as a RepositoryException wrapping the original error
The implementation class lives in lib/data/repositories/ and imports only from lib/domain/ and the Supabase adapter (217)
Row-level security (RLS) on the contact_chapter_affiliations table is not bypassed — the implementation uses the authenticated Supabase client

Technical Requirements

frameworks
Flutter
Riverpod
Supabase
apis
Supabase PostgREST API (contact_chapter_affiliations table)
supabase-contact-chapter-adapter (component 217)
data models
ChapterAffiliation
ContactChapter
MaxAffiliationsExceededException
AffiliationNotFoundException
AlreadyAssignedToChapterException
performance requirements
getChaptersForContact issues a single Supabase query (no sequential per-chapter lookups)
addChapterAffiliation issues at most 3 Supabase operations: count check, duplicate check, insert — consider combining count+duplicate into a single query if Supabase supports it
All operations complete within 2 seconds on a standard mobile connection
security requirements
Use authenticated Supabase client; never use the service-role key on the client side
Validate contactId and chapterId are non-empty strings before sending to Supabase
RLS policies on contact_chapter_affiliations must restrict reads to the authenticated user's organization — verify policy exists before relying on it

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use Supabase's .count() modifier on the select query for the pre-insert count check rather than fetching all rows and counting in Dart — this is a single round-trip and accurate under concurrent writes. For the duplicate check, use .eq('contact_id', contactId).eq('chapter_id', chapterId).maybeSingle() — if result is non-null, throw AlreadyAssignedToChapterException. Note that Supabase PostgREST does not support multi-statement transactions natively from the client; if atomicity between count-check and insert is critical, implement this as a Supabase database function (RPC) called via .rpc(). Coordinate with the backend team on whether an RPC is needed or if an optimistic insert with a unique constraint + error catch is acceptable.

Testing Requirements

Write integration tests using a Supabase test environment (or a local Supabase instance via Docker) for the full CRUD cycle: add first affiliation, add fifth affiliation, attempt sixth (expect exception), remove, attempt to remove non-existent (expect exception). Also write unit tests with a mocked Supabase client to verify the count-check and duplicate-check logic without network calls. Integration tests go in test/integration/repositories/; unit tests in test/data/repositories/. Run unit tests in CI; gate integration tests separately.

Component
Contact Chapter Repository
data medium
Epic Risks (3)
high impact medium prob technical

The Cross-Chapter Activity Query must avoid N+1 fetches across chapters. If naively implemented as a per-chapter loop, it will cause severe performance degradation for contacts affiliated with 5 chapters on poor mobile connections.

Mitigation & Contingency

Mitigation: Design the query as a single PostgREST join of contact_chapters and activities on contact_id from the start. Add a query performance test with 5 affiliations and 100+ activities to the integration test suite and enforce a maximum execution time threshold.

Contingency: If a performance regression is detected post-merge, introduce a Supabase RPC function (stored procedure) to move the join server-side, bypassing any client-side N+1 pattern.

high impact low prob security

If the Duplicate Warning Event Logger write fails silently (network error, RLS denial), audit entries will be missing from the Bufdir compliance record without the user being aware.

Mitigation & Contingency

Mitigation: Implement the logger with a local fallback queue: if the Supabase write fails, persist the event locally and retry on next launch. Log all failures to a verbose output channel.

Contingency: Add a reconciliation job that compares locally queued events to Supabase entries and re-submits any gaps. Provide a data export of the local queue for manual audit if reconciliation fails.

medium impact low prob technical

Two coordinators simultaneously adding the 5th chapter affiliation for the same contact could bypass the maximum enforcement check if both reads occur before either write completes.

Mitigation & Contingency

Mitigation: Enforce the 5-affiliation maximum as a database-level constraint (CHECK + trigger or RPC with a FOR UPDATE lock) rather than relying solely on application-layer validation.

Contingency: If a constraint violation is detected in production, run a corrective query to end the most recently created excess affiliation and notify the relevant coordinator.