high priority low complexity backend pending backend specialist Tier 4

Acceptance Criteria

isCacheStale({int maxAgeMinutes = 30}) returns true when last_synced_at is null (cache was never populated)
isCacheStale() returns true when the elapsed time since last_synced_at exceeds maxAgeMinutes
isCacheStale() returns false when the elapsed time is within maxAgeMinutes
isCacheStale() is a synchronous or fast-async method that does not trigger a network call
clearCache() deletes all rows from the cached contacts and notes Drift tables
clearCache() resets last_synced_at to null so isCacheStale() returns true after clearing
clearCache() is called during logout flow to remove all PII from the device
The staleness threshold is injectable/configurable at the call site — callers can pass a custom maxAgeMinutes without modifying the repository
The key used for last_synced_at in SharedPreferences is namespaced by organisationId to support multi-org users
All three methods (isCacheStale, clearCache, and last_synced_at persistence in syncContactsForOrganisation) are covered by unit tests

Technical Requirements

frameworks
shared_preferences for last_synced_at persistence
drift for clearCache() table deletion
flutter_test for unit testing
data models
contact
performance requirements
isCacheStale() must resolve in under 5ms — SharedPreferences read is synchronous after initialisation
clearCache() must complete in under 100ms for typical cache sizes (< 1000 rows)
security requirements
clearCache() must be integrated into the logout flow to ensure no contact PII persists after sign-out
last_synced_at key must be namespaced per-organisation to prevent cross-org data assumptions in multi-org accounts

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Compute staleness using `DateTime.now().toUtc().difference(lastSyncedAt).inMinutes > maxAgeMinutes`. Store last_synced_at as a Unix timestamp (milliseconds since epoch) in SharedPreferences for reliable cross-timezone comparison. Namespace the key as `contact_cache_last_synced_` to support multi-org users. For clearCache(), use Drift's `delete(contactsCache).go()` and `delete(notesCac he).go()` wrapped in a transaction, then call `prefs.remove(key)`.

Expose the staleness threshold as an optional named parameter with a default of 30 rather than a constructor constant — this makes it trivial for callers to override without subclassing. Document the logout integration requirement in a code comment so future developers do not accidentally remove the clearCache() call from the auth flow.

Testing Requirements

Unit tests using an in-memory Drift database and a mocked SharedPreferences instance. Cover: (1) isCacheStale returns true when no last_synced_at is stored, (2) isCacheStale returns true when stored timestamp is older than maxAgeMinutes, (3) isCacheStale returns false when stored timestamp is within maxAgeMinutes, (4) isCacheStale respects custom maxAgeMinutes parameter, (5) clearCache deletes all rows from contacts and notes tables, (6) clearCache resets last_synced_at so subsequent isCacheStale call returns true, (7) keys are correctly namespaced per organisationId — clearing org A cache does not affect org B timestamp.

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).