Implement Supabase RLS-scoped ilike search queries
epic-contact-search-data-layer-task-003 — Implement the SupabaseSearchRepository class against the contacts and notes Supabase tables. Write parameterised ilike queries scoped by the authenticated user's organisation via RLS. Support search by contact name, organisation affiliation, and notes keywords. Return a unified ContactSearchResult list sorted by relevance (name match first).
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Place the class in lib/features/contact_search/data/remote/supabase_search_repository.dart. Inject the SupabaseClient via constructor for testability. For the notes join, use Supabase's nested select syntax: supabase.from('notes').select('id, content, contact_id, contacts(id, name, organisation, chapter_affiliation, role)').ilike('content', '%$keyword%'). Map the nested contacts object in the JSON response to ContactSearchResult.
For deduplication in search(), use a Map
Testing Requirements
Write unit tests using a mocked SupabaseClient (Mockito or Mocktail). Test: (1) searchByName with a valid query calls .from('contacts').select().ilike('name', ...) and maps the response to ContactSearchResult list correctly, (2) searchByNotes calls the notes table and joins contact data, (3) search() deduplicates a contact that appears in both name and notes results — the deduplicated result keeps matchedField.name, (4) a PostgrestException from the mock client causes a SearchRepositoryException to be thrown. Do NOT write integration tests against real Supabase in this task — those belong in a separate integration test suite. Use flutter_test for all tests.
Supabase RLS policies may not correctly scope ilike search results to the authenticated user's organisation and chapter, causing data leakage across organisations or empty result sets for valid queries.
Mitigation & Contingency
Mitigation: Reuse and extend existing RLS query builder patterns from the contact-list-management feature. Write integration tests against a seeded multi-organisation test database to verify cross-org isolation before merging.
Contingency: If RLS scoping is insufficient, add an explicit organisation_id filter in the Dart query builder layer as a defence-in-depth measure while the Supabase policy is corrected.
Adding new Drift tables for the contact cache may conflict with existing migrations or schema versions in the contact-list-management feature if both features cache the same contacts table, causing migration failures on user devices.
Mitigation & Contingency
Mitigation: Audit existing Drift schema versions from contact-list-management before writing new migrations. Reuse existing cache tables if the schema already covers required fields; only add missing fields via ALTER or new version.
Contingency: If schema conflict occurs, consolidate into a single shared cache table owned by contact-list-management and expose a DAO interface to the search feature, avoiding duplicated schema ownership.
The offline cache may surface significantly stale contact data if sync has not run recently, leading coordinators to act on outdated information (wrong phone numbers, changed assignments).
Mitigation & Contingency
Mitigation: Store and surface the last-sync timestamp prominently in the UI layer. Trigger a background cache refresh on app foreground when connectivity is detected.
Contingency: If staleness becomes a reported UX issue, implement a maximum-age threshold that shows a warning banner when the cache is older than a configurable limit (e.g. 24 hours).