critical priority medium complexity integration pending integration specialist Tier 1

Acceptance Criteria

ContactSearchService routes queries to SupabaseSearchRepository when connectivity state is ConnectivityResult.wifi or ConnectivityResult.mobile
ContactSearchService routes queries to OfflineSearchRepository (Drift) when connectivity state is ConnectivityResult.none
Routing decision is made at the time of each search call β€” a query issued while online uses Supabase; a query issued 1ms later while offline uses Drift, without any manual intervention
The UI layer (BLoC, Riverpod notifier, or widget) does not need to know which repository is active β€” ContactSearchService exposes a single search(String query) method
When connectivity transitions from offline to online mid-session, the next search call uses Supabase automatically
When connectivity transitions from online to offline mid-session, the next search call uses Drift automatically
If SupabaseSearchRepository throws a network error despite the connectivity check returning online (race condition), ContactSearchService falls back to OfflineSearchRepository and returns a SearchResult with an isFromCache: true flag
ContactSearchService injects both repositories and the connectivity provider via constructor β€” no hard-coded dependencies
Unit tests for routing logic pass using mocked repositories and a fake connectivity stream
No import of package:connectivity_plus or Supabase client directly in UI widgets β€” routing is entirely encapsulated in ContactSearchService

Technical Requirements

frameworks
connectivity_plus
riverpod (for DI of ContactSearchService) or BLoC
rxdart (optional, for stream composition)
flutter_test
mockito or mocktail
apis
connectivity_plus Connectivity().onConnectivityChanged stream
SupabaseSearchRepository.search()
OfflineSearchRepository.search()
data models
contact
ContactSearchResult
performance requirements
Routing decision must add less than 1ms overhead to each search call
Connectivity stream subscription must be a single shared subscription β€” not re-subscribed on every search call
security requirements
ContactSearchService must not cache or log query strings
Fallback to offline cache must only occur after a network failure β€” not pre-emptively β€” to ensure fresh Supabase data is preferred
RLS policies on Supabase enforce per-organisation data isolation β€” the routing layer must not bypass or cache credentials

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use constructor injection for both repositories and the connectivity stream: ContactSearchService(this._supabaseRepo, this._offlineRepo, this._connectivityStream). In the class body, subscribe to _connectivityStream in the constructor and cache the latest ConnectivityResult in a private field _currentConnectivity. Each search() call reads _currentConnectivity synchronously β€” no awaiting the stream on every call. The fallback logic (Supabase throws β†’ use Drift) should be a try/catch around the Supabase call; catch SocketException and AuthException specifically, not all exceptions (a Supabase 400 Bad Request should bubble up as an error, not silently fallback to cache).

The isFromCache flag on SearchResult is important for the UI to optionally show a 'showing cached results' banner β€” include it in the model even if the UI does not use it initially. Register ContactSearchService as a singleton in the Riverpod provider tree so its connectivity subscription persists for the app lifetime.

Testing Requirements

Unit tests in test/features/contact_search/contact_search_service_test.dart. Mock both repositories with mockito/mocktail. Provide a StreamController as the fake connectivity source. Test cases: (1) online state → SupabaseRepository called, OfflineRepository not called, (2) offline state → OfflineRepository called, SupabaseRepository not called, (3) transition online→offline → second query uses OfflineRepository, (4) Supabase throws NetworkException while 'online' → fallback to Offline, result has isFromCache=true, (5) empty query after sanitisation → neither repository called, empty SearchResults returned.

All tests must be synchronous or use fake_async β€” no real timers.

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

Cancelling in-flight Supabase HTTP requests via RxDart switchMap may not actually abort the server-side query if the Supabase Dart client does not support request cancellation tokens, leading to wasted API calls and potential race conditions where a slow earlier response arrives after a faster later one.

Mitigation & Contingency

Mitigation: Audit the supabase-flutter client's cancellation support before implementation. Use RxDart switchMap to discard stale emissions even if HTTP cancellation is unavailable, ensuring only the latest result reaches the UI.

Contingency: If race conditions surface in testing, add a query sequence counter to tag each emission and discard any response whose sequence number is lower than the most recently emitted one.

medium impact low prob technical

Connectivity detection used to route between online and offline repositories may have latency or give false positives on flaky connections, causing the service to attempt Supabase queries that time out instead of falling back to the cache promptly.

Mitigation & Contingency

Mitigation: Use a try/catch with a short timeout on Supabase calls. On network error, immediately fall back to the offline repository and emit a cached result with an offline indicator rather than surfacing an error state.

Contingency: If the timeout-based fallback proves insufficient, implement a connection health check stream that pre-validates connectivity before each query batch.