A reading and study app that works with no network, and reconciles when one returns
A reading app across two long texts, with the study features that make one worth using — highlighting, bookmarks with titles, notes, search, adjustable type, a night mode, and memory of where you were.
None of that is difficult on its own. What made it an engineering problem is where reading actually happens: on a plane, on a commute, in a building with no signal, in places with expensive data. The app is expected to be complete offline, which means the network cannot be in the path of anything the reader does — and it means the app owns a local database whose contents have to be reconciled with a server later.
I worked on the frontend, the local database and the sync layer.
Offline-first sounds like caching and is not.
A cache is a copy you can throw away. Here the local database holds things that exist nowhere else at the moment they are created: a highlight made on a train, a note typed in a basement. Losing any of it is unacceptable in a way that losing a cached page is not.
So three problems compound. The reading interface must never wait on a network call, which means it reads from local storage and only local storage. Every change has to be recorded in a way that can be replayed later, in order, against a server that has moved on. And when the same record has changed on both sides, something has to decide — and the decision has to be one the user would agree with, because they cannot see it happening.
The last of those is where naive sync goes wrong. Last-write-wins is easy to implement and quietly deletes the note somebody spent five minutes writing.
I built the React Native frontend, the SQLite schema the app reads from, and the sync layer between local storage and the backend — including how changes are recorded, replayed and reconciled.
The design decision underneath all of it: the local database is the source of truth for the interface, not a mirror of the server. Every screen reads locally. The network is a background process that reconciles, never something a screen waits for.
That inverts the usual arrangement, and it costs more to build. What it buys is that offline stops being a mode. There is no degraded state to design, no spinner while the app decides whether it has a connection, no difference between a user with signal and one without. The app is simply always fast, and sometimes also up to date.
SQLite on the device holds the texts and everything the reader creates — highlights, bookmarks, notes, reading position and history. Every screen queries it directly.
User changes are written locally and recorded as pending work rather than fired at the network. A background pass drains that queue when a connection exists, applies changes to the backend, and pulls down anything that changed elsewhere. Because the queue is ordered and durable, closing the app mid-sync is not an event — the work is still there next time.
Reconciliation is deliberately biased. Where both sides changed the same record, the resolution favours keeping what the user wrote over what the server holds, because a duplicate note is a minor annoyance and a deleted one is a betrayal.
The interface never reads from the network. Sync is a background reconciliation, not a request path.
↳ Reads only from local storage
↳ Durable across app restarts
↳ Background, never blocking
The same path whether or not there is a network.
Write locally
SQLite insert; the interface updates from local state
Record intent
The change is queued as pending work, durably
Wait
No network required; the reader carries on
Drain
Background pass sends pending changes in order
Pull
Anything changed elsewhere comes down
Reconcile
Conflicts resolved in favour of keeping the reader's work
Most of the care went into the boundary between the interface and storage. Long-form reading is unforgiving about frame rate — a list that stutters while scrolling scripture is immediately noticeable — so queries are shaped for the screen that needs them rather than fetching broadly and filtering in JavaScript.
The pending-change queue is the other half. It has to be durable, ordered, and safe to replay: the app can be killed at any moment, and the same change being applied twice must not produce two highlights. Reanimated carries the interface animation on the UI thread so reading stays smooth while a sync is running underneath.
There is no sync status to interpret, because there is nothing the reader could usefully do with it. The app is the same app with or without a connection. Type size, night mode and reading position are treated as part of the reading experience rather than settings buried in a menu.
Stated qualitatively on purpose. Invented percentages are the easiest thing in the world to write and the fastest way to lose a technical reader.
The app behaves the same way with the network off, so there is no degraded state for a user to discover at the worst moment.
Highlights and notes made without a connection reconcile without being overwritten by server state.
Local queries shaped per screen, and animation on the UI thread, kept long scrolling sessions from stuttering.
A reading app where the network is a background detail rather than a prerequisite — local storage as the source of truth, durable ordered change replay, and conflict resolution that treats a reader's own words as the thing worth protecting.
If this sounds familiar
Building something similar?
Most of these problems show up again in different clothes. Describe yours and I will tell you what I would look at first.