high priority medium complexity backend pending backend specialist Tier 5

Acceptance Criteria

A `searchQueryProvider` (StateProvider<String>) or equivalent internal field holds the current search query string
When searchQuery is non-empty, the provider exposes a `filteredContacts` AsyncValue derived from ContactSearchService using the current query
When searchQuery is cleared (set to empty string), `filteredContacts` reverts to the full unfiltered role-scoped list from the cached full-list fetch — no new network request is made
The unfiltered `contacts` AsyncValue in the provider is never replaced or mutated during a search operation
If the full list is in an error state when a search is attempted, filteredContacts reflects the same error state
If the full list is in loading state when a search is attempted, filteredContacts reflects loading state
Query changes during an in-flight remote search cancel the previous search and start a new one (debounce handled by ContactSearchService)
Consecutive identical queries do not trigger duplicate service calls
Widget tests confirm: typing in search field updates filteredContacts, clearing field restores full list without loading indicator
Provider state remains consistent if the full contact list refreshes while a search is active — the search re-runs against the new list

Technical Requirements

frameworks
Flutter
Riverpod (StateProvider, ref.watch, ref.listen)
flutter_riverpod
apis
ContactSearchService.search(String query, List<Contact> contacts)
data models
Contact
ContactListState (adding: filteredContacts: AsyncValue<List<Contact>>, searchQuery: String)
performance requirements
Clearing the search query must restore the full list in zero network round-trips
Query state updates must not cause the unfiltered contact list provider to re-fetch
Filtered list computation (local path) must complete synchronously within one frame
security requirements
Search query string must not be persisted between app sessions
Query state must not be shared across different authenticated user sessions
ui components
SearchTextField (AppTextField variant)
ContactListView (consuming filteredContacts)

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Model the search query as a `StateProvider` at the module level rather than an internal field on the AsyncNotifier — this allows the search text field widget to write the query directly via `ref.read(searchQueryProvider.notifier).state = newQuery` without going through the notifier's update methods. The `ContactListRiverpodProvider` should `ref.watch(searchQueryProvider)` and derive `filteredContacts` reactively: `if (query.isEmpty) return state.contacts else return ContactSearchService.search(query, state.contacts)`. This pattern ensures filteredContacts is always a pure derivation — never independently stored — which eliminates the possibility of stale filtered state. For the remote search path (large datasets), `filteredContacts` should be an `AsyncValue` that wraps the debounced future from ContactSearchService.

Use `ref.listen(searchQueryProvider, (prev, next) { ... })` inside the notifier if you need side effects on query change, but prefer the reactive `ref.watch` derivation where possible. Reset `searchQueryProvider` to empty string in the notifier's `ref.onDispose` to prevent stale query state if the provider is later re-initialised.

Testing Requirements

Unit tests using ProviderContainer: verify filteredContacts equals full list when query is empty, verify filteredContacts is filtered subset when query is non-empty, verify clearing query returns full list without incrementing mock service call count. Widget tests: simulate typing in AppTextField, confirm ContactListView rebuilds with filtered results, confirm clearing text restores full list. Test edge case: full list refresh while query active — filtered results update to reflect new list. Use fake ContactSearchService to make local-path tests deterministic.

Test debounce cancellation via fake async timers for the remote path scenario.

Component
Contact List Riverpod Provider
data medium
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.