← All posts

Offline is the default, not a mode

Every read in weekkii hits local SQLite and sync is background reconciliation. Here is what that architecture costs, and why we pay it.

Most apps that advertise "offline support" mean a cache and an apology. You get a stale snapshot of whatever you last loaded, a banner explaining that you are offline, and a spinner the moment you try to change anything. Offline is treated as a failure state, a mode the app falls into and spends all its energy trying to escape.

We built weekkii the other way around. There is no offline mode because the network was never in the read path. Every read hits a local SQLite database: expo-sqlite on iOS and Android, IndexedDB-backed on web. Every write commits locally first, then joins a queue. Sync is a background reconciliation job that pushes queued writes and pulls server changes whenever a connection happens to exist. When there is no connection, nothing in the app is waiting for one.

The plane, the subway, the passport line: the week is just there. Not a cached copy that might be stale. The actual week, readable and editable, because the database it lives in is on your device.

What this costs

We want to be honest about the price, because this architecture is not free. The hard part of local-first is not working offline. It is coming back online.

The same task can be edited on two devices while both are offline. When they reconnect, those edits have to merge. The common answer is last-write-wins on timestamps, but timestamps across devices are a lie: clocks drift, and "last" depends on whose clock you believe. We use a dirty-wins rule instead. When a pull brings down the server's copy of a task that has unsynced local edits, the local edit wins. The change you made on the device in your hand beats the copy the server is holding.

Encryption makes all of this harder than it would otherwise be. weekkii is end-to-end encrypted: the server stores ciphertext sealed with NaCl secretbox, and it has no key. A server that cannot read your tasks also cannot merge them. There is no server-side conflict resolution, no clever arbitration in Postgres, because Postgres only ever sees opaque bytes. Every merge happens on the client, after decryption, on data only your devices can read.

And there is a limit we want to state plainly. If you edit the same field of the same task on two offline devices, one of those edits wins and the other is gone. No merge rule can read your mind about which version you meant. We chose the one you can see on the device in your hand, because a sync that silently overwrites what is on your screen is exactly the kind of surprise that makes people stop trusting an app.

The same principle shows up in smaller places. Reminders in weekkii are local notifications scheduled on-device. That is not just an offline nicety. The server could not read your tasks to send you a reminder even if we wanted it to, so the only place a reminder can be scheduled is the device that holds the key.

This is a trade, and we made it with open eyes. Local-first plus client-side merging is more code and more edge cases than a thin client talking to a smart server. It is the right trade for a planner, because a plan you cannot open is not a plan.

So when a phone shows no bars, the week opens anyway. A task gets checked off, another gets added to Thursday. Nothing spins, nothing apologizes, and later, on some wifi nobody even notices, the sync quietly catches up.