high priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

A public constant `kLocalSearchThreshold` (int, default 200) is defined in ContactSearchService or a dedicated constants file and controls the path-selection boundary
Local path is invoked when the contact list size is strictly less than kLocalSearchThreshold
Search matches contacts where the normalised name OR normalised notes contains the normalised query as a substring
Query normalisation applies: trim whitespace, convert to lowercase, apply Unicode NFC normalisation (using Dart's String.normalize or equivalent package)
An empty or whitespace-only query returns the full unfiltered contact list
Search is case-insensitive: query 'ØST' matches contact named 'Øst-Oslo'
Search handles Norwegian special characters: ø, æ, å (and uppercase equivalents) match correctly
Search on a null notes field does not throw — treats null as empty string
Returns an empty list (not null) when no contacts match
Unit tests cover: exact match, partial match in name, partial match in notes, no match, empty query, null notes, Norwegian character match, mixed-case query

Technical Requirements

frameworks
Flutter
Dart (dart:core String)
data models
Contact (name: String, notes: String?)
performance requirements
Local filter must complete in under 50ms for a list of 199 contacts on a mid-range device
No async operations — local path is synchronous for immediate UI responsiveness
security requirements
No query string is sent to any external service in the local path
Normalised query must not be stored or logged

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use `String.toLowerCase()` for lowercasing — Dart's toLowerCase() is locale-aware for ASCII but for Norwegian characters it is safer to normalise to NFC first using the `characters` package or `String.runes`. Consider using the `diacritic` pub package if full Unicode folding is needed, but for Norwegian (ø→ø, æ→æ, å→å) simple toLowerCase() after NFC normalisation is sufficient since these characters do not decompose under NFC. The local filter should be a pure function `List _filterLocal(List contacts, String normalisedQuery)` — this makes it trivially testable. Expose `kLocalSearchThreshold` as a top-level constant in a `contact_search_constants.dart` file so it can be overridden in tests without subclassing.

Do not use RegExp for substring matching — `String.contains()` is sufficient and avoids regex injection risk.

Testing Requirements

Pure unit tests (flutter_test) with no mocks needed — local filtering is deterministic. Create a fixture list of 10–20 Contact objects covering: ASCII names, Norwegian names (ø/æ/å), contacts with null notes, contacts with notes containing keywords. Test each acceptance criterion as a separate test case. Verify the threshold constant is accessible and has the correct default value.

No widget tests or integration tests required at this layer.

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.