critical priority high complexity database pending database specialist Tier 2

Acceptance Criteria

A concrete class SupabaseCrossChapterActivityQuery implements CrossChapterActivityQuery
fetchActivitiesForContact issues exactly one Supabase query (verifiable in Supabase logs or by inspecting the Dart code — no loops that call Supabase)
The query uses Supabase PostgREST foreign key embedding or a raw RPC call to JOIN contact_chapters → chapters and contact_chapters → activities in a single round-trip
Results are ordered by activity_date descending
The flat JSON response is parsed into a List<CrossChapterActivityResult> with all fields correctly mapped
chapterName is populated from the chapters table join, not hardcoded
If the contact has no affiliations or no activities, an empty list is returned (not an exception)
PostgrestException is caught and rethrown as a typed domain exception (e.g., ActivityQueryException)
The authenticated user's RLS context is respected: activities from chapters the user cannot access are not returned
A performance test (or manual verification in Supabase logs) confirms a single query is issued for a contact with 5 chapters, each with 100 activities

Technical Requirements

frameworks
Flutter
Supabase
BLoC
apis
Supabase PostgREST (select with foreign key embedding or .rpc())
Supabase Auth
data models
activities
contact_chapters
chapters
contacts
performance requirements
Single SQL round-trip regardless of the number of chapters the contact belongs to
Query must use indexed columns: contact_chapters.contact_id, activities.chapter_id, activities.activity_date
Expected query time < 200ms for a contact with 5 chapters and up to 500 total activities
Parsed result list construction must not use nested loops that are O(n²)
security requirements
Never pass contactId directly into a raw SQL string — use parameterized queries or PostgREST .eq() filters
RLS on both activities and contact_chapters tables must be active; the implementation must not use service_role key
Validate that contactId is a valid UUID format before sending to Supabase to prevent injection

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

PostgREST foreign key embedding syntax for a join: supabase.from('contact_chapters').select('chapter_id, chapters(name), activities(id, activity_type, activity_date, duration_minutes, summary, is_billable)').eq('contact_id', contactId).order('activity_date', referencedTable: 'activities', ascending: false). Note that PostgREST returns a nested JSON structure; you will need to flatten it: for each contact_chapter row, extract chapter_id and chapters.name, then iterate the embedded activities array. This is still a single HTTP call. Alternatively, use a Supabase RPC (stored procedure) for more control over the SQL.

The RPC approach is preferable if the join involves more than 2 levels of nesting or if you need DISTINCT ON for deduplication. Create the RPC in a migration file and call it via supabase.rpc('get_cross_chapter_activities', params: {'p_contact_id': contactId}). Ensure the function is defined with SECURITY INVOKER (not DEFINER) so it runs under the caller's RLS context.

Testing Requirements

Write unit tests using a mocked SupabaseClient: (1) mock a successful response with 3 activities across 2 chapters and verify the result list contains 3 CrossChapterActivityResult objects with correct chapterName values; (2) mock an empty response and verify an empty list is returned; (3) mock a PostgrestException and verify ActivityQueryException is thrown. Write integration tests against a local Supabase instance: seed contact_chapters with 3 chapter affiliations, seed activities for each chapter, call fetchActivitiesForContact, and assert all activities are returned in date-descending order; verify that activities from a 4th chapter (not affiliated) are not returned. Use flutter_test for all tests.

Component
Cross-Chapter Activity Query
data high
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.