The App That Has to Work Without Internet
The moment an app is most useful is often the moment the internet is not working. The decisions and the trade-offs of offline-first architecture, seen through the design of Sağlam and the QR verification in Vakti Geçmeden.
The thing that kept nagging at me while I was designing Sağlam was this: the moment the app becomes useful is exactly the moment the internet stops working. In the first hours after an earthquake the network is either weak or congested. You don't add an "offline mode" to an app like that later on — either it is built that way from the start, or it never happens.
Offline is not a feature, it is an assumption
Most apps are written on this assumption: there is internet, and if there isn't, it will be back shortly. Caching gets layered on top of that assumption afterwards; we store the server's response so the screen opens quickly next time. That is a good solution for speed, but it is not working offline.
In an offline-first architecture the assumption is inverted: everything needed to draw the screen is already on the device. The network exists to refresh that data, not to make it appear. I measure the difference with a single question: if I install the app on a device that has never seen the internet and open it in airplane mode, what does the user see?
If the answer is a blank screen or a spinner, that app doesn't work offline; it only remembers that it worked once. The difference shows up at exactly the moment the user needs the app most.
Every cache-based solution is empty on the first launch. At that moment the device holds only two things: what you shipped inside the app bundle, and what you deliberately had it download earlier. The offline architecture has to be built on top of those two sources.
Three things that have to sit on the device
While working on Sağlam I split the offline requirement into three parts. That split made the whole thing concrete very quickly, because all three differ in size, in how often they need refreshing, and in how they break.
- Data. Assembly areas, evacuation points, building and ground information. It rarely changes and its size is manageable.
- Map. The road network, building blocks, the ground layer. This is what carries the weight, and this is where things stumble most.
- Model. The calculation itself that produces the risk score and the evacuation route. This is the piece that gets skipped most often.
Here is why I list the third one separately: if you download the data and the map to the device but run the calculation on the server, you have gained nothing. When the user asks "where do I go from here," the answer still comes over the network. Both the logic that produces the score and the route calculation have to run on the device. What is left for the server is updating the coefficients and the data package.
Carrying the map in a single file
The usual approach on the map side is to download tiles from the network as the user pans around and accumulate them in a cache. That is reasonable in an ordinary app; it does not work in a scenario like this one. Because the region the user has never panned over may be exactly the region they will need in an emergency. A cache fills up according to the past, not according to need.
So in the design I treated the map as a file. PMTiles keeps all the tiles in one single file and the reader only reads the byte range it needs; MapLibre can take that file directly as a source and render it. What it buys you is simple but significant: there is no tile server, no thousands of small files; there is one single object to download and version-check.
uygulama/
├─ assets/
│ └─ tr-temel.pmtiles // country-wide, low detail — bundled in the app
└─ belgeler/ // regions downloaded on first launch
├─ istanbul.pmtiles
├─ toplanma-alanlari.json
└─ surum.json
Verifying the file's integrity, swapping it for a newer one when it goes stale, deleting it in one go when you need to free up space — all of it stays simple because it is a single file. Try doing the same jobs with thousands of tile files and you end up wrestling with the file system more than with the offline architecture itself.
The real tension: size or coverage
Everything up to this point looks sensible. The real decision point is in this question: how much of this data ships inside the app?
| Approach | What it buys you | Where it breaks |
|---|---|---|
| Bundle everything in the app | Works completely even on the first launch | The download size balloons; it runs into store limits and into the user's patience |
| Download the region once | Reasonable size, coverage tailored to the user | If you miss the download window, the device stays empty |
| Accumulate tiles as the user pans | Wastes no unnecessary space | The region you don't have when you need it turns out to be exactly the one you need |
The solution I designed for Sağlam is a mix of the first two. A low-detail base layer covering the whole country is bundled into the app; even if the device has never seen the internet, the user can see a map, their own location and a rough list of the nearest assembly areas. The detailed regional package is downloaded during the initial setup, while the user is still calm and still has a connection.
That second part is less an engineering problem than a timing problem. Asking for the download at the moment the user needs it defeats the offline architecture entirely. The app's job is to be ready long before the need arises.
The stores have upper limits on download size, and before you hit those, the user's patience comes into play. A large package means abandoned installs and people giving up on downloading over mobile data. Before you widen the coverage, you have to ask "is this layer really needed offline" separately for every single layer.
Verification without internet: the QR side of Vakti Geçmeden
A much smaller version of the same problem came up in Vakti Geçmeden. A QR code is displayed on one device, and another device scans and verifies it. If verification is done by asking the server, the system stops working exactly where the connection is worst — in a crowd, indoors, in a basement.
The solution was to make the code carry itself. The QR contains the information to be verified along with its signature; the scanning device already has the key to check that signature embedded in it. Verification therefore happens entirely on the device, the result is written to a queue, and it is sent to the server once a connection comes back.
// Pseudocode — verification order
kod = qrOku()
if (imzaGecersiz(kod)) return REDDET
if (kod.gecerlilikSonu < suAn) return SURESI_DOLMUS
if (yerelKullanilanlar.iceriyor(kod.id)) return ZATEN_KULLANILDI
yerelKullanilanlar.ekle(kod.id)
kuyruk.ekle({ islem: "kod_kullanildi", id: kod.id })
return KABUL // the server gets the final say once the connection is back
There is a limit to this approach that has to be stated honestly: while offline, you cannot prevent the same code from being used on two different devices. The local list only protects your own device. That is why the validity window has to be kept short, and why the server has to remain the final authority for operations that carry a real cost. Offline verification is there to keep the door open, not to settle the account.
Where the cloud's job begins
Firebase is still there on the back end, but its role changes. In an offline-first app the cloud does these three things:
- Updating the packages. Publishing new versions of the map and data files.
- Collecting the queue. Picking up the operations that piled up on the device, in order, once a connection is available.
- Having the final say. Confirming or reversing the provisional decisions the device made.
Firestore's own local cache takes on part of this job and genuinely helps; but on its own it is not an offline architecture. The cache holds data that has been read before, and it will not hand you data that was never read on the first launch. That is why the sentence "Firestore already works offline" is not true in the initial-setup scenario.
To tell whether the package on the device has gone stale, a small standalone surum.json is enough. Checking it first when a connection is available, downloading the new package in the background if needed, and switching over in one step only after the download finishes removes most of the half-finished-update problems.
The hard part: testing it
The easiest way to test offline behavior is airplane mode, but that is also the easiest scenario. What actually breaks apps is not the absence of a connection, it is a half-present one. The request goes out and no response comes back; the download is cut off midway; the signal drops and returns every couple of seconds.
My own checklist is this:
- Clean install, a device that has never seen the internet, first launch in airplane mode.
- Cutting the connection during the package download and killing the app.
- Not fully offline, but a slow and intermittent connection.
- A device whose disk is nearly full.
- Operations performed in airplane mode going out in the correct order once the connection returns.
Then there is the user-facing side: showing that the app is offline and how old the data in hand is. A line that reads "Last updated: 3 days ago" is far more honest than quietly showing stale data.
Summary
Working offline is not a checkbox added to the settings screen. The moment you decide to move the data, the map and the calculation onto the device, the app's size, its update flow and the server's role all change from the ground up. And it becomes impossible to think about those three separately.
That is part of why I am still keeping Sağlam in the design phase: if these decisions are made wrong, fixing them later means rewriting the app. I saw the same thing while moving QR verification onto the device in Vakti Geçmeden — a feature that works where there is no internet forces you to rethink everything around it.
- Offline
- Architecture
- Maps
- Flutter
- Firebase
Demir Taşdemir
Mobile App & Web Developer
I have been building software since 2018. I have published 11 apps on the App Store and Google Play; right now I am working on 6 mobile apps, 1 e-commerce platform and 1 desktop game.
Do you have a product that has to work offline?
Let's talk about how far it makes sense to move the data, the map and the calculation onto the device.