If every screen waits for Supabase, the app feels unreliable on mobile networks. If Room is merely an unstructured fallback, local and remote state drift.
Use Room as the observable source for UI:
val workouts: Flow<List<Workout>> = dao.observeWorkouts(userId)
Refresh pulls remote changes into a transaction. Local writes update Room immediately and enter a pending-sync state. A worker later pushes them and records acknowledgement.
Each record needs stable identity and enough metadata for conflict handling. Deletions require tombstones until remote acknowledgement or another device may restore them.
Separate freshness from content. Existing local records plus a failed refresh is different from an empty account. Expose last successful sync and pending writes where the product benefits.
Authentication changes require partitioning or clearing user-owned local data. Never show the previous account's cache while a new session initializes.
Supabase can expose database-shaped APIs, but mobile features should still use repositories and domain mappings. That boundary protects UI from schema details and makes offline policy testable.
Test process death after local write, duplicate pulls, delayed acknowledgements, logout, and two devices editing the same record. Offline-first is a consistency model, not a cache flag.