critical priority medium complexity database pending database specialist Tier 2

Acceptance Criteria

A Supabase migration file exists that adds a CHECK constraint or BEFORE INSERT trigger on contact_chapters enforcing a maximum of 5 rows per contact_id
Attempting to insert a 6th affiliation for the same contact_id raises a database-level error (constraint violation or trigger exception) with a descriptive error message
The Dart ContactChapterRepository catches the database constraint violation and throws a typed DomainException (e.g., AffiliationLimitExceededException) before it reaches the BLoC layer
RLS policy on contact_chapters allows SELECT only for rows where chapter_id matches the authenticated coordinator's assigned chapter(s)
RLS policy on contact_chapters allows INSERT/UPDATE/DELETE only when the target chapter_id is within the coordinator's scope
Global admins and org admins bypass chapter-scoped RLS and can read/write all rows
A coordinator from chapter A cannot read or modify affiliations belonging to chapter B
The migration is idempotent: running it twice does not produce errors (uses IF NOT EXISTS guards)
All existing unit tests for ContactChapterRepository pass after the constraint is added
Manual test: inserting 5 affiliations succeeds; inserting a 6th returns the typed exception in Dart

Technical Requirements

frameworks
Flutter
BLoC
Supabase
apis
Supabase PostgREST
Supabase Auth (JWT claims for RLS)
data models
contact_chapters
contacts
chapters
coordinator_chapter_assignments
performance requirements
Constraint check must be enforced at the database layer to avoid race conditions from concurrent inserts
RLS policies must use indexed columns (contact_id, chapter_id) to avoid full-table scans
Repository-layer count check (before insert) is advisory only; database constraint is authoritative
security requirements
RLS must be ENABLED on contact_chapters — never disabled in production
JWT claims used in RLS policies must be set server-side and never trusted from client input
The coordinator's chapter scope must be derived from auth.uid() joined to coordinator_chapter_assignments, not from a client-supplied parameter
Migration must not grant BYPASSRLS to the anon or authenticated roles

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use a PostgreSQL trigger function rather than a plain CHECK constraint if the count needs to span multiple rows — CHECK constraints cannot reference aggregate counts across rows. The trigger pattern: BEFORE INSERT ON contact_chapters, count existing rows for NEW.contact_id, raise exception if count >= 5. In the Dart repository, wrap the Supabase insert call in a try/catch for PostgrestException; inspect the code field ('P0001' for RAISE EXCEPTION from trigger, '23514' for CHECK). Map these to AffiliationLimitExceededException so BLoC can present a user-friendly message.

For RLS, create two policies: one FOR SELECT using EXISTS (SELECT 1 FROM coordinator_chapter_assignments WHERE user_id = auth.uid() AND chapter_id = contact_chapters.chapter_id), and an identical one FOR ALL (INSERT/UPDATE/DELETE). Add a separate USING (true) policy for the service_role. Test locally with supabase db reset and supabase db push before committing the migration.

Testing Requirements

Write unit tests in flutter_test for the Dart repository layer: (1) mock Supabase client returning a PostgrestException with code '23514' (CHECK violation) or '23505' and assert that the repository throws AffiliationLimitExceededException; (2) mock a successful insert for counts 1–5 and assert no exception. Write integration tests against a local Supabase instance (via supabase_test or Docker): verify the constraint fires on the 6th insert; verify a coordinator JWT can read only their chapter's rows and receives an empty result set for other chapters; verify an org admin JWT reads all rows. Test idempotency by running the migration SQL twice in CI.

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.