Offline-First Is a Data Model Decision, Not a Caching Strategy
Treating the local database as the source of truth changes what your app is. What that buys, what it costs, and why a password vault is the easy case.
"Offline support" usually arrives late, as a cache in front of a network call. The app still believes the server holds the truth; the cache is a politeness for when the truth is briefly unreachable.
Offline-first inverts that. The local database is the source of truth. The network, if there is one at all, is a peer that synchronises with it.
That is not a bigger cache. It changes what the application is.
What actually changes
In a network-first app, the UI is a function of a request. Every screen has three states — loading, error, content — and every one of them has to be designed, built and tested.
In an offline-first app, the UI is a function of the local database. Reads are synchronous and always succeed. There is no spinner, because there is nothing to wait for.
Network-first Offline-first
───────────── ─────────────
UI UI
└─ request └─ local database ◄── source of truth
└─ network └─ sync (optional, background)
└─ cache └─ networkThree consequences follow directly:
Writes complete immediately. The user's change is committed locally and the UI reflects it. No optimistic-update bookkeeping, no rollback path, no half-applied state.
Latency stops being a UI concern. A local query is sub-millisecond. The loading states that dominate a network-first codebase largely disappear.
Errors move. They do not vanish — they relocate from "every screen" to "the sync layer", where they can be handled once and retried in the background.
The cost, stated honestly
The bill arrives if and when you add synchronisation.
The moment the same record can be edited in two places, you own a distributed systems problem, and no framework makes it disappear. You have to answer:
- What is the unit of conflict? A whole entry, or an individual field? A password and a note edited on different devices are not really in conflict — unless your conflict unit says they are.
- How is order determined? Wall-clock timestamps are tempting and wrong; device clocks disagree. Logical clocks — a version vector, or a CRDT — cost more to implement and actually work.
- What does the user see when it goes wrong? "Your change was overwritten" is unacceptable for a password manager, where the overwritten value may be the only copy of a credential.
This is the honest trade: offline-first removes a great deal of incidental complexity from the UI and concentrates the remaining, essential complexity in one hard layer.
Why a password vault is the easy case
NexusPass gets the benefits without most of the bill, because of a property of the problem rather than any cleverness in the design.
There is no server. The vault is a file on the device. Not "cached locally" — that is where it lives. Nothing to sync, no conflicts, no distributed state, no operator.
Which makes the data layer small enough to state in a sentence: an encrypted file, opened into memory on unlock, written back on change, closed on lock.
The shape looks like this:
interface VaultRepository {
// A cold flow over local state. Never touches a network.
fun entries(vaultId: VaultId): Flow<List<Entry>>
// Suspends only for disk and crypto — never for a request.
suspend fun upsert(entry: Entry)
suspend fun delete(entryId: EntryId)
}Note what is missing: no Result wrapper for network failure, no isLoading,
no refresh policy, no cache invalidation. Those concepts have no referent here.
The parts that still need care
Removing the network does not remove every hard problem. Two remain, and both are consequences of the data being sensitive rather than remote.
Writes must be atomic. An encrypted vault written in place and interrupted by a crash or a battery death is a corrupted vault — and for a local-only product, a corrupted vault may be an unrecoverable one. The fix is old and reliable: write to a temporary file, fsync, then atomically rename over the original. A partially written temp file is discardable; a half-written vault is not.
Plaintext has a lifetime. Holding decrypted entries in memory is what makes reads instant, and it is also the largest window an attacker has. That is what auto-lock is for: it is not a convenience feature, it is a bound on how long the sensitive state exists.
Choosing between them
Offline-first is the right default when the data is primarily yours — notes, tasks, a vault, a journal, local media. Single-writer data with no genuine need to reconcile.
It is the wrong default when the data is inherently shared and freshness is the point: a chat, a live dashboard, a collaborative document, anything with inventory or availability. There, the network is the source of truth, and pretending otherwise means building a synchronisation engine you did not want.
The question that separates them is not "should this work offline?" — almost everything should degrade gracefully. It is "whose copy is authoritative when two disagree?" Answer that first, and the architecture follows.