high priority low complexity backend pending backend specialist Tier 3

Acceptance Criteria

A single public method `search(String query, List<Contact> currentContacts)` (or equivalent) dispatches to local or remote path based on `currentContacts.length < kLocalSearchThreshold`
Query normalisation is implemented as a single private method `_normalise(String query): String` called by both local and remote paths — no duplication
The debounce duration is exposed as a constructor parameter with a default of `const Duration(milliseconds: 300)`
Switching from a large dataset (remote) to a small dataset (local) mid-session correctly uses the local path without carrying over any pending debounce timer
Switching from local to remote mid-session correctly initialises the debounce timer fresh
The public API surface of ContactSearchService does not expose path-selection internals — callers always call the same method regardless of dataset size
Unit tests confirm path selection boundary: list of 199 → local, list of 200 → remote (using kLocalSearchThreshold = 200)
Normalisation unit tests: 'ÅSE ' normalises to 'åse', ' Øst ' normalises to 'øst', empty string normalises to empty string

Technical Requirements

frameworks
Flutter
Dart (dart:async Timer)
data models
Contact
ContactSearchService (internal routing logic)
performance requirements
Path selection decision is O(1) — based on list.length, no iteration
Normalisation method is O(n) in query length — acceptable for typical search strings under 100 chars
security requirements
Normalisation must not alter the semantic meaning of the query in a way that produces false positives against sensitive contact data
Path selection must not be bypassable by a caller passing a manipulated list size

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

The `_normalise` method should be a static private method or a top-level function in a companion file — this makes it independently testable without instantiating ContactSearchService. Implementation: `query.trim().toLowerCase()`. NFC normalisation via the `characters` package is recommended for robustness but verify it handles all three Norwegian vowels correctly before adding the dependency. The path selection should read `contacts.length` passed in at call time, not store a cached count — this avoids stale routing after list updates.

Consider making `ContactSearchService` a `ChangeNotifier`-free plain Dart class: Riverpod's provider wraps it, and the service itself stays pure. Document the threshold constant with a comment explaining why 200 was chosen (e.g., empirical testing showed local filtering stays under 50ms for up to 200 contacts on a low-end device).

Testing Requirements

Unit tests (flutter_test) for path selection boundary conditions: size = threshold-1 (local), size = threshold (remote), size = threshold+1 (remote). Test normalisation method independently with Norwegian characters, leading/trailing whitespace, and empty input. Test that switching dataset size between calls routes correctly. No integration tests needed at this layer — repository interactions are covered in task-005 tests.

Use dependency injection to supply a mock for the remote path in path-selection tests to avoid real network calls.

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.