Skip to Content
ArchitectureOffline & sync (superseded)

Offline data loading and sync

⚠️ Status: superseded. The mobile consolidation (2026-06) removed the Android Room/offline layer, the core-data library, the per-feature Hilt modules, and the sync-algorithm-design.md shared sync stub. This document describes the previous architecture. Post-consolidation, all clients are online-first (network-only). Offline and sync capabilities are being re-evaluated; this doc is retained as a reference for what was previously built and what may be restored.

Background (what was removed)

The Android app previously had:

  • Room local databases for tasks, habits, and other feature data (stale-while-revalidate cache)
  • Firebase Remote Config for data policy mode (cache_fallback, force_online, remote_only)
  • StaleWhileRevalidateCoordinator in core-data — returned cached data immediately, refreshed from network in background
  • BaseRepository with doRequest / doRemoteRequest pattern
  • SyncUtils + WorkManager workers for batch offline sync of tasks/habits
  • core-data, core-ui, and test-core Gradle modules

These were removed as part of the mobile consolidation (branch design-system-foundation) — see tech-debt/migrations.md for details.

Current state (post-consolidation)

AreaStatus
Local cachingNone. All reads hit the server.
Offline supportNot available. No offline queue, no stale cache.
SyncNot available. The shared Ktor SyncService stub exists but is not wired.
Data policyNetwork-only. DataPolicyMode still exists in shared but is not used by any repository.
DesktopOnline-first (unchanged).
WasmNot a shipped client.

Future direction

The offline/sync architecture is being re-evaluated. Likely approaches under consideration:

  1. Re-introduce a shared offline cache using a multiplatform local database (e.g., SQLDelight, or a simple DataStore-backed queue) — this would restore the soft-UX pattern across all platforms, not just Android.
  2. Keep network-only and document it as a product decision, prioritizing real-time sync and optimistic UI instead.
  3. Reintroduce sync only (without offline cache) — the SyncService stub in shared could be completed for batch operations.

See the sync-algorithm-design.md for the original design goals and the tech-debt register item TD-08 for current status.

Historical architecture (removed)

Data policy modes

ModeEnumRole
cache_fallbackCacheFallbackDefault stale-while-revalidate: local first + background refresh
force_onlineForceOnlinePrefer freshness; always attempt remote fetch
remote_onlyRemoteOnlyNetwork-only reads

Resolution order (Android, removed)

  1. Firebase Remote Config key data_policy_mode
  2. BuildConfig.DATA_POLICY_MODE_FALLBACK from env OTER_DATA_POLICY_MODE
  3. Safe default cache_fallback

Repository tiers (removed)

TierMeaningExamples
ARoom + remote; local-first + background refreshTasks, habits
BPartial local cacheWorkout, nutrition
CNetwork-onlyFinance, dashboard, auth

Manual sync contract (removed)

SyncUtils.sync ran WorkManager-triggered batch sync for tasks and habits:

  1. Load last_sync_time from preferences
  2. SyncRepository.getLocalSyncData() — local pending changes
  3. SyncRepository.sync() — server processes batch
  4. Apply deltas ordered by action (INSERT, UPDATE, DELETE) with remote-ID mapping

Known limitations

  • Not every screen yet exposes refresh state in UI state.
  • No per-feature data policy overrides (only global mode).
  • Desktop does not run Firebase Remote Config.
  • Wasm is not a shipped product client — do not assume Wasm parity with Android.
  • shared/.../policy/DataPolicyMode.kt, DataPolicyProvider.kt — still in shared module
  • shared/.../services/sync/SyncService.kt — Ktor stub (unwired)
  • composeApp/.../FirebaseDataPolicyProvider.kt, EnvDataPolicyProvider.kt — still present for potential re-use

Manual sync contract (tasks / habits)

SyncUtils.sync (Android) runs only when NetworkHelper.isNetworkAvailable():

  1. Load last_sync_time from preferences.
  2. SyncRepository.getLocalSyncData(lastSyncTimestamp) — local pending changes.
  3. SyncRepository.sync(localSync) — server processes batch.
  4. On success: apply tasks and habits deltas via SyncHelper.syncData (ordered by action: INSERT, UPDATE, DELETE), then map tasksSynced / habitsSynced remote IDs back onto local rows, then preferences.saveLastSyncTime(lastTimeStamp).

Ordering of operations matters for referential consistency; keep INSERT before UPDATE/DELETE per entity stream when extending.

Background sync (WorkManager)

Periodic and boot-driven scheduling lives under composeApp (SyncDataWorkerImpl calls SyncUtils.sync; WorkManagerUtilsImpl enqueues work; BootReceiver can trigger runWorkManagerTasks). OterApp may comment out automatic enqueue on cold start — check the current line before assuming sync runs at launch.

Write path and local consistency (audit)

AreaBehavior
Tasks / habitsMutations use doRequest with remote + local paths; list consistency after remote success relies on repository implementation + sync for pending offline work.
SWR list refreshSuccessful background refresh calls mergeRemoteSnapshotWithoutHistory (tasks/habits) to avoid deleting unsynced local rows.
WorkoutMutations use doRemoteRequest where local mirror is not implemented — UI should refetch or rely on next navigation.
FinanceRemote-first writes; no Room mirror in tier C — presentation handles errors softly.
Recipes / nutritionFollow doRequest + preferences; confirm screen-level refetch after mutations where needed.

Gaps should be fixed in-repo or called out in feature guides when product requires strong offline write parity.

Supported clients

  • Android: Remote Config + BuildConfig / env fallback; Room; SyncUtils + WorkManager triggers as wired in app module.
  • Compose Desktop: Ktor online-first; EnvDataPolicyProvider only; no Room sync stack equivalent to Android.
  • Wasm: Not in use for this product — no client work or parity guarantees.

Known limitations

  • Not every screen yet exposes refresh state in UI state; tasks/habits use domain events for refresh failures.
  • Per-feature RC overrides (e.g. data_policy_finance) are not implemented; only global data_policy_mode.
  • Desktop does not run Firebase Remote Config.
  • shared/.../policy/DataPolicyMode.kt, DataPolicyProvider.kt
  • core-data/.../StaleWhileRevalidateCoordinator.kt, BaseRepository.kt
  • composeApp/.../FirebaseDataPolicyProvider.kt, EnvDataPolicyProvider.kt
  • composeApp/.../SyncUtils.kt