What really separates a good app from a truly great one? It’s often not the flashy features, but something much more fundamental: the ability to just work, no matter what your internet connection looks like. This guide explains the architecture that makes that happen, offline-first, and shows how to build it step by step with a real tool.
What happens when the internet drops?
You’re on a subway, driving through a dead zone, or your Wi-Fi just decides to quit on you. What happens to the app you’re using at that very moment? For most apps, the answer isn’t pretty. You tap a button and nothing happens. You get a loading spinner that spins forever. Or the content you were just looking at vanishes, replaced by an ugly error message.
There are three problems behind this:
- Connectivity breakdown: apps that rely on a live internet connection can’t access or update data when offline.
- User experience disruption: users get slow performance or errors, which leads to frustration.
- Data loss and inconsistency: changes made during the disconnection may be lost or cause conflicts when connectivity returns.
What is offline-first architecture?
The best apps you use every day don’t have this problem, because they plan for that exact moment. The core idea is simple but powerful: the app is designed to work first without any internet connection. It treats syncing with a server as a secondary task, something that happens in the background whenever it can.
In a traditional app, the app asks the server for data, waits, and then maybe shows you something. In the offline-first model, the app talks to a database right there on your device, shows the UI instantly, and then worries about syncing. It’s a total flip of the script.
You already use this every day: opening WhatsApp on the subway to read old chats, pulling up a saved area in Google Maps with zero signal, or accessing your notes in Notion on a plane. The app feels reliable because its core job doesn’t depend on a live connection.
Offline-first vs local-first
The two terms overlap, but they are not the same. It helps to look at how software evolved:
- Desktop apps: the first wave ran locally on users’ computers, reading and writing files on disk. They worked offline and your data was private, but it was trapped on one device.
- Cloud apps: software moved online. Users could access data from any device and collaborate. But network latency can make apps slow, and without internet you can’t access your data.
- Offline-first: developers found that retrofitting cloud apps to work offline was complicated and prone to insidious bugs, so the idea became to design apps to work offline from the start rather than as an afterthought.
- Local-first: users no longer truly owned and controlled their data in the cloud. Local-first aims to get the best of both worlds: the local ownership and control of old-school apps, plus the cloud’s convenient cross-device access and multi-user collaboration.
The researchers who described local-first listed seven ideals for end users. Two overlap directly with offline-first, two are about keeping the advantages of cloud apps, and the remaining three are about long-term access to software, privacy and control over data.
The three critical pieces
Offline-first apps boil down to three pieces:
- A local database on your device that acts as the main source of truth.
- A smart sync engine, kind of like the brain of the operation, managing all the data transfer in the background.
- A clear strategy for what to do when things get messy, called conflict resolution.
The key mindset shift: the local database is king. It is the single source of truth for what the user sees. The app doesn’t need to ask the internet for permission to show you your own data. It just shows you what it has locally right now.
Reading and writing in the data layer
Offline-first starts in the data layer: the repositories and the data sources they delegate to. A repository with network access needs at least two data sources, a local one and a network one, and it is the repository’s job to populate the local data source from the network one.
Each data source often has its own model. For example, a news app may have an author entity from the local SQL database and a network author from the server. Those differences stay inside the data layer, and a third model, author, is exposed to the rest of the app.
Reading
Expose reads through observable APIs (such as Kotlin Flows), because data can change between connectivity states and the rest of the app needs to know when it does. The rule is that external layers must always read from the local data source, which keeps data consistent between connectivity states. The network read just updates the local data.
Writing
Use asynchronous APIs (such as Kotlin coroutine suspend functions), because writes cross a network boundary and must not block the UI thread. Writes need more care than reads because of conflicts. An app doesn’t even need to write data offline to count as offline-first.
If any step fails, check whether the operation should be retried. If not, terminate. Otherwise requeue it to be performed later, and persist the result when successful.
Synchronization and conflicts
When connectivity is restored, the app must reconcile its local data with the network. This is called synchronization. There are two main styles:
| Pull-based | Push-based | |
|---|---|---|
| How it works | The app reaches out to read the latest data on demand, often screen by screen | The local source mimics a replica of the network source, fetches a baseline at first startup, then relies on server notifications |
| Pros | Relatively easy; unneeded data is never fetched | Can stay offline indefinitely; minimal data use after first sync; works well for relational data |
| Cons | Heavy data use when screens are revisited; scales poorly with relational data | Versioning for conflicts is non-trivial; concurrency concerns; must be supported by the network |
Conflict resolution
Imagine you edit a note offline while your collaborator edits the same note online. That’s a conflict, and you must resolve it before synchronization can happen. Sometimes the last change made is the one that sticks, called last write wins. Other times the server always gets the final say. It’s a critical design decision.
In the timeline: both devices are offline. Device A writes locally, then Device B writes conflicting data. Both come back online and sync. Device B’s write overwrites A’s on the network, and Device A is then notified and updated to match what was last written.
Build it with PowerSync and Supabase
You can build this yourself, or use a tool made for it. PowerSync is a sync engine that gives your app an embedded SQLite database (a database that lives in one local file) and syncs it with a backend database such as Postgres on Supabase. Users work with the local database, changes sync to the backend automatically, and changes from other users flow back.
Setup steps
- Create a PowerSync instance and add a connection to your Supabase Postgres database. You need the host, port, database name and user from Supabase’s connect settings, and your database password.
- Test the connection, and run a SQL query to check that you can read your remote data (a simple to-dos table works well).
- Write sync rules that decide what each user receives: all rows, only some rows, or only rows belonging to the logged-in user.
- In the app, define a schema that mirrors your table (for example
todoswith a name and a done flag; the ID column is added automatically). - Initialize the PowerSync database and let it know when the user logs in, so the sync rules apply.
- Listen for Supabase real-time events (insert, update, delete) so remote changes reach the local database.
What the demo proves
In the FlutterFlow demo, a to-do list works whether you have internet or not. Create, update and delete work locally. Status icons show whether the app is online and synced; they turn red briefly when many quick changes are batched, then green again. Sync is bidirectional: edit in the app and it appears in Supabase, edit in Supabase and it appears in the app.
Then the real test: turn the internet off, delete items, add items, and nothing breaks. Reconnect, and everything syncs automatically. The user never feels disconnected and can always complete their tasks.
Tip for reactive UIs: in FlutterFlow, work with App State for anything the UI shows, and call notify-listeners after local changes so the page refreshes.
Strategies, pitfalls and when to use it
It’s not a one-size-fits-all deal. Pick the strategy that fits the job:
| App type | Strategy |
|---|---|
| News app | Cache-first: show recent articles |
| Messaging app (like WhatsApp) | Fully local-first |
| E-commerce app | Hybrid: keep the cart offline, load the large product catalog online |
Be honest about the pitfalls: the architecture is more complex, your app size gets bigger, managing data conflicts can become a serious headache, and the phone’s operating system may kill your background sync process.
Use it for productivity tools where a seamless experience is everything, or when users are often in places with spotty internet. Skip it for a simple admin dashboard that is always used on a stable office connection. That’s probably overkill.
The interview playbook
If an interviewer asks you to design an app like WhatsApp, here is a strong answer:
- Declare the local database as the source of truth.
- Add a background sync worker (the sync engine).
- Implement optimistic UI updates: the app assumes an action like sending a message will succeed later, so it shows “sent” instantly and feels fast.
- Define your rules for handling data conflicts.
FAQ
What is offline-first?
An architecture where the app is designed to work without any internet connection first, and treats syncing with a server as a background task.
What is the difference between offline-first and local-first?
Offline-first is about working without a connection. Local-first adds ownership and control of your data, along with cross-device access and collaboration.
What is last write wins?
A conflict strategy where the change written last is the one that is kept.
Do I need a sync engine?
Not strictly, but syncing, queuing and conflicts are hard to get right, so tools like PowerSync save a lot of effort.
Is offline-first always worth it?
No. It trades upfront complexity for a seamless user experience, so use it where your users really need it.
Conclusion
If you remember just one thing, let it be this: the user interface never waits for the internet. That is the fundamental promise of offline-first architecture. Start with a local database as the source of truth, add a sync engine in the background, and decide your conflict rules up front. As expectations for fast, reliable apps keep going up, this approach is becoming less of a niche technique and more of a default standard for building high-quality applications.