high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

Remote path is invoked when the contact list size is greater than or equal to kLocalSearchThreshold
Debounce is applied: rapid successive calls within the debounce window cancel the previous pending request and issue only one Supabase query
Supabase query uses ILIKE on both name and notes columns with OR logic: `.or('name.ilike.%query%,notes.ilike.%query%')`
The query string passed to Supabase is the same normalised string used by the local path (trim, lowercase, NFC)
An empty or whitespace-only query after normalisation cancels any pending debounced call and returns the full contact list (or triggers a fresh full-list fetch)
Results are returned as List<Contact> mapped through the same Contact.fromJson factory used elsewhere
Network errors (timeout, no connection, Supabase error) are thrown as typed exceptions and not swallowed
A cancelled in-flight request (due to debounce) does not emit a result or error — it is silently discarded
Debounce duration is configurable via a constant (default 300ms) and injectable for testing
Integration test verifies that only one Supabase call is made when three rapid queries are issued within the debounce window

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Dart client
apis
Supabase PostgREST contacts table (ILIKE filter, OR clause, RLS-enforced)
data models
Contact
ContactRepository.searchContacts(String query): Future<List<Contact>>
performance requirements
First result after debounce window must appear within 2 seconds on a standard mobile connection
Cancelled requests must release their resources immediately — no memory leaks from dangling Futures
Supabase query must use an index on name and notes columns (coordinate with DB migration task)
security requirements
Query string is passed as a PostgREST parameter, not interpolated into a raw SQL string — no SQL injection risk
RLS on the contacts table ensures users can only search within their authorised scope
No raw contact data is cached at the service layer between searches

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement debounce using a `Timer` (dart:async) held as a nullable field in the service. On each new query call, cancel any existing timer and schedule a new one. When the timer fires, perform the Supabase call. Use a `Completer>` pattern or expose a `Stream>` so the caller can await the debounced result cleanly.

A `StreamController.broadcast()` approach is recommended: each call to search() emits into the stream after the debounce fires, allowing Riverpod to `watch` the stream directly. The ContactRepository.searchContacts() method should use `.or()` with ILIKE rather than two separate `.ilike()` calls to avoid AND semantics. Ensure the debounce timer is cancelled in a dispose() method to prevent timer leaks when the service is garbage collected. For testability, inject the debounce duration as an optional constructor parameter with a default of `const Duration(milliseconds: 300)`.

Testing Requirements

Unit tests (flutter_test) with a mocked ContactRepository to verify: normalisation applied before passing to repo, empty query short-circuits without calling repo, repo errors propagate correctly. Integration test (test with a real or emulated Supabase instance) to verify: ILIKE query returns expected matches, OR logic across name+notes works, RLS blocks cross-org results. Debounce behaviour test: use fake async / Timer mocking to confirm only one repo call fires after three rapid invocations within the window.

Component
Contact Search Service
service low
Epic Risks (2)
medium impact medium prob technical

For organizations with large contact lists (NHF has 1,400 local chapters and potentially thousands of contacts), local in-memory filtering may be too slow and Supabase ILIKE queries without supporting indexes may exceed acceptable response times or accumulate excessive read costs, degrading search usability for power users.

Mitigation & Contingency

Mitigation: Define and document the list-size threshold in ContactSearchService before implementation. Confirm that indexes on name and notes columns exist in the Supabase schema before enabling server-side search. Profile ContactSearchService against realistic data volumes in the staging environment using the largest expected org.

Contingency: If response times are unacceptable in staging, introduce result-count pagination in ContactListService and add a user-visible 'showing top N results — refine your search' indicator, deferring full pagination to a follow-up task.

high impact low prob security

In NHF's multi-chapter context, when a user switches organization, Riverpod providers may emit a brief window of stale contact data scoped to the previous organization before the invalidation cycle completes, transiently exposing contacts from the wrong chapter.

Mitigation & Contingency

Mitigation: Model organization context as a Riverpod provider dependency so that any context change immediately marks contact providers as stale. Render a loading skeleton instead of the stale list during the re-fetch transition. Cover this scenario in integration tests with explicit org-switch sequences.

Contingency: If race conditions are observed during QA, add an explicit organization_id equality check in ContactListService that compares each fetched record's scope to the active session org, discarding any mismatched batch before returning results to the provider.