critical priority medium complexity database pending database specialist Tier 0

Acceptance Criteria

Migration file is created in the Supabase `migrations/` directory with a timestamp-prefixed filename (e.g., `20260329000001_create_contact_chapters.sql`)
The `contact_chapters` table has a composite primary key on `(contact_id, chapter_id)`
Foreign key from `contact_id` to `contacts.id` with `ON DELETE CASCADE` to automatically remove chapter affiliations when a contact is deleted
Foreign key from `chapter_id` (organisation unit ID) to `organization_units.id` with `ON DELETE CASCADE`
An index exists on `contact_id` alone to support 'fetch all chapters for contact' queries
An index exists on `chapter_id` alone to support 'fetch all contacts in chapter' queries
A `CHECK` constraint enforces that no single `contact_id` appears more than 5 times in the table (enforcing the 5-chapter maximum rule from the NHF requirement)
`created_at TIMESTAMPTZ NOT NULL DEFAULT now()` and `updated_at TIMESTAMPTZ NOT NULL DEFAULT now()` columns are present
An optional `role_in_chapter TEXT` column is included to store the contact's role within the chapter (matching the `contact_chapter` data model)
A `joined_at TIMESTAMPTZ NOT NULL DEFAULT now()` column is present (matching the domain model)
The migration is idempotent: running it twice does not produce errors (use `IF NOT EXISTS`)
The migration includes a corresponding rollback section (or a separate down migration) that drops the table and all its indexes cleanly

Technical Requirements

frameworks
Supabase
apis
Supabase PostgreSQL 15 migrations CLI
data models
contact_chapter
contact
performance requirements
Composite PK index and the two single-column indexes must be created; query plans for both 'by contact' and 'by chapter' lookups must use index scans
Table must be created with `UNLOGGED` option evaluated and rejected in favour of durability — this is transactional data
security requirements
Table must be created without granting public access — RLS will be enabled in the subsequent task
Run `ALTER TABLE contact_chapters ENABLE ROW LEVEL SECURITY;` at the end of this migration so the table is locked down before any policies are defined
No service-role bypass granted to the `anon` or `authenticated` roles in this migration

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

The CHECK constraint for the 5-chapter maximum is best implemented as a constraint trigger or a deferred constraint rather than a simple column CHECK, because the limit applies across multiple rows for the same `contact_id`. A plain `CHECK` on a single row cannot count sibling rows. Use a `BEFORE INSERT` trigger function that counts existing rows for the incoming `contact_id` and raises an exception if `count >= 5`. Alternatively, use a PostgreSQL partial unique index approach with a counted view, but the trigger approach is cleaner for this use case.

Name the trigger `trg_contact_chapters_max_five` for discoverability. Ensure the `updated_at` column is kept current via a standard `moddatetime` trigger or a `BEFORE UPDATE` trigger calling `NEW.updated_at = now()`.

Testing Requirements

After applying the migration to a local Supabase instance, verify with `psql` or the Supabase Studio table editor: (1) table exists with correct column types; (2) inserting 5 rows for the same contact_id succeeds; (3) inserting a 6th row for the same contact_id fails with a CHECK constraint violation; (4) deleting a row from `contacts` cascades to `contact_chapters`; (5) EXPLAIN ANALYZE on `SELECT * FROM contact_chapters WHERE contact_id = $1` shows an index scan. Document these verification steps as a migration test checklist in the PR description.

Component
Supabase Contact Chapter Adapter
infrastructure medium
Epic Risks (2)
high impact medium prob security

Supabase RLS policies for a junction table that spans organisations are non-trivial. An incorrectly scoped policy could expose chapter affiliations from other organisations to coordinators, constituting a data breach.

Mitigation & Contingency

Mitigation: Draft RLS policies in a staging environment and run an explicit cross-organisation isolation test suite before merging. Use Supabase policy testing tools and peer review all policy definitions.

Contingency: If a policy error reaches review, roll back the migration and apply a corrective patch. Ensure no production data has been exposed by auditing Supabase logs for cross-organisation query results.

medium impact medium prob technical

The contact_chapters table migration may conflict with existing foreign key structures or require a backfill for contacts already assigned to a single chapter, causing migration failures in production.

Mitigation & Contingency

Mitigation: Write the migration as an additive, non-destructive operation. Backfill existing single-chapter assignments by deriving them from the existing contact records. Test the full migration on a production-sized dataset clone before release.

Contingency: Provide a rollback migration script that removes the new table without touching existing contact records. Coordinate with operations for a maintenance window if a re-run is needed.