critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

ContactCacheSyncRepository is a concrete class with a syncContactsForOrganisation(String organisationId) async method returning Future<SyncResult>
syncContactsForOrganisation() fetches all contacts scoped to organisationId from Supabase using the authenticated user's JWT (RLS enforced server-side)
Associated notes for fetched contacts are fetched in the same sync operation, either via a join or a follow-up query
Fetched Supabase records are mapped to Drift companion objects before being passed to the DAO upsert methods
On successful fetch+upsert, last_synced_at is persisted (SharedPreferences or Drift settings table) with the current UTC timestamp
SyncResult contains: itemsSynced (int), durationMs (int), and an optional error field
If the Supabase fetch fails with a network error, the method completes with SyncResult.failure — it does not throw or crash
If the Drift upsert fails, the method rolls back the transaction and returns SyncResult.failure without corrupting existing cache data
The method can be called concurrently — a second call while a sync is in progress either waits or returns immediately with a 'sync already in progress' result
No UI imports or BuildContext references appear in this class — it is a pure data-layer repository

Technical Requirements

frameworks
drift for local cache persistence
supabase_flutter SDK for remote data fetch
shared_preferences or Drift settings table for last_synced_at persistence
riverpod or bloc for dependency injection of the repository
apis
Supabase PostgREST REST API — contacts table with RLS
Supabase PostgREST REST API — notes table filtered by contact_ids
data models
contact
assignment
performance requirements
Full sync of 200 contacts and their notes must complete in under 3 seconds on a stable connection
Use a single Supabase query with an embedded join (select: '*,notes(*)') rather than N+1 queries per contact
Drift bulk upsert called once per entity type per sync, not per record
security requirements
Supabase client must use the authenticated user session — never the service role key on the mobile client
RLS on the contacts and notes tables ensures only organisation-scoped records are returned — no additional client-side filtering required, but document this assumption
last_synced_at stored in SharedPreferences does not contain PII
On user logout, clearCache() must be called to wipe the Drift tables and reset last_synced_at

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use Supabase's embedded resource syntax to fetch contacts and notes in one round trip: `supabase.from('contacts').select('*, notes(*)').eq('organisation_id', organisationId)`. Map the nested JSON to Drift companions in a pure transformation function (testable in isolation). Use a boolean `_isSyncing` flag or a Mutex from the `synchronized` package to prevent concurrent sync races. Define a SyncResult sealed class (success/failure) rather than throwing exceptions — callers should not need try/catch.

Inject both the Supabase client and the DAOs via the constructor for testability. Store last_synced_at as an ISO-8601 UTC string in SharedPreferences under a key namespaced by organisationId to support multi-org accounts.

Testing Requirements

Integration-style unit tests with a mocked Supabase client and an in-memory Drift database. Cover: (1) happy path — mock returns 10 contacts with notes, verify Drift tables contain 10 rows after sync and last_synced_at is set, (2) empty organisation — mock returns 0 contacts, verify SyncResult.itemsSynced == 0 and no error, (3) Supabase network failure — mock throws PostgrestException, verify SyncResult.failure returned and Drift tables are unchanged, (4) Drift write failure — stub DAO to throw, verify SyncResult.failure and last_synced_at not updated, (5) concurrent calls — call syncContactsForOrganisation() twice in parallel, verify only one sync proceeds and both futures resolve without exception.

Component
Contact Cache Sync Repository
data low
Epic Risks (3)
high impact medium prob security

Supabase RLS policies may not correctly scope ilike search results to the authenticated user's organisation and chapter, causing data leakage across organisations or empty result sets for valid queries.

Mitigation & Contingency

Mitigation: Reuse and extend existing RLS query builder patterns from the contact-list-management feature. Write integration tests against a seeded multi-organisation test database to verify cross-org isolation before merging.

Contingency: If RLS scoping is insufficient, add an explicit organisation_id filter in the Dart query builder layer as a defence-in-depth measure while the Supabase policy is corrected.

medium impact medium prob integration

Adding new Drift tables for the contact cache may conflict with existing migrations or schema versions in the contact-list-management feature if both features cache the same contacts table, causing migration failures on user devices.

Mitigation & Contingency

Mitigation: Audit existing Drift schema versions from contact-list-management before writing new migrations. Reuse existing cache tables if the schema already covers required fields; only add missing fields via ALTER or new version.

Contingency: If schema conflict occurs, consolidate into a single shared cache table owned by contact-list-management and expose a DAO interface to the search feature, avoiding duplicated schema ownership.

medium impact medium prob scope

The offline cache may surface significantly stale contact data if sync has not run recently, leading coordinators to act on outdated information (wrong phone numbers, changed assignments).

Mitigation & Contingency

Mitigation: Store and surface the last-sync timestamp prominently in the UI layer. Trigger a background cache refresh on app foreground when connectivity is detected.

Contingency: If staleness becomes a reported UX issue, implement a maximum-age threshold that shows a warning banner when the cache is older than a configurable limit (e.g. 24 hours).