# How to build a web app that still works without internet

> Most web apps show a blank screen in airplane mode. Here is how a service worker keeps your app loading and usable offline — and which parts genuinely cannot work without a connection.

Source: https://northerngo.com/resources/offline-first-web-apps-pwa/
Language: en
Updated: 2026-08-15

---
**A published NorthernGo app installs a service worker on its first online visit, and after that it opens in airplane mode with its layout, styling and already-seen images intact instead of a blank screen. What cannot work offline is anything live: database reads and writes, login, e-mail and cloud AI go straight to the network and fail cleanly.**

### Why most web apps break offline

Open almost any web app in airplane mode and you get a blank white screen. The reason is simple: the browser has nothing stored locally, so when the network request for the page fails, there is nothing to fall back on. A Progressive Web App only fixes this if it ships a service worker that actually caches the app — being installable to the home screen is not the same thing as working offline.

### Publishing is what gives the app a service worker

Apps published to a NorthernGo subdomain or a custom domain are served with a service worker at `/sw.js` and a web app manifest at `/manifest.json`. Publish from **My Projects**, then open the app once while you still have a connection. That first visit is what installs the service worker and fills the cache — there is no way around it, since the files have to be downloaded at least once.

### The address you publish to decides whether any of this applies

This part is worth being blunt about, because it is the single most common reason someone follows an offline guide and gets nothing.

The service worker and the manifest are only served on the app's own host. There are three ways a NorthernGo app can be reached, and they do not behave the same:

- **A custom domain** (Pro). The app is the whole site at that address, so `/sw.js` and `/manifest.json` resolve and the offline cache works.
- **`<subdomain>.northerngo.com`** (Premium and Pro). Same thing — the subdomain serves the app as the top-level document, with the manifest linked in the head and the service worker registered on load.
- **The `https://northerngo.com/?app=<projectId>` share link.** This one gets no service worker and no manifest, because those paths on the main domain belong to the marketing site rather than to your app. Free plan projects use this fallback, which means a Free plan app has no offline story at all.

So the practical prerequisite for everything below is a paid plan and a claimed address. Use **Create PWA** on the project card to claim a subdomain, or [bind a custom domain with SSL](/resources/connect-custom-domain-ssl/) if you are on Pro.

### What gets cached

Three things are stored on the device during that first visit:

1. **The app shell** — the HTML page itself, so navigation offline serves the cached copy instead of failing.
2. **Styling and icon resources** — the Tailwind and icon CSS the app loads from a CDN, so it renders styled rather than as unformatted text.
3. **Images the app has already displayed** — anything not cached falls back to a neutral placeholder instead of a broken image icon.

### How each kind of request is actually handled

The four rules below are the whole caching policy, and knowing them tells you exactly what a user will see when the connection drops.

- **Page navigation is cache-first.** If a cached copy of the page exists, it is served immediately without asking the network. This is what removes the blank screen, and it has a cost — see the trade-off below.
- **Images are network-first, then cache, then a placeholder.** A fresh image wins when there is a connection; offline, a previously seen image comes from the cache; an image the device has never loaded resolves to a plain grey SVG that says the image is not available offline.
- **Other static resources use stale-while-revalidate.** Stylesheets, fonts and CDN scripts are served from the cache instantly and quietly refreshed in the background, so the app renders fast offline and stays current online.
- **Anything that looks like an API call is never cached.** Requests to `/api/` paths and to the database and platform hosts bypass the cache entirely. Only `GET` requests are eligible for caching in the first place, so every `POST` your app makes goes straight to the network.

### What cannot work offline

This is where honesty matters more than marketing. Login, e-mail, payments and cloud AI still go straight to the network and fail when there is none. The service worker does not cache those responses.

Database `save()` and `delete()` on a published PWA (subdomain or custom domain) are different: the SDK queues them in `localStorage` and replays the queue when the app is online again. `get()` returns the last successful list plus any pending records, marked `_pending: true`. `window.NorthernGo.offlineStatus()` reports `{ online, pending }`. Login and register are never queued.

The practical consequence: build your app so the interface renders immediately, then calls `get()`. An app built that way opens offline, shows last-known data, and only login, payments, e-mail and AI announce that they need a connection. Do not invent a second write queue in app code.

### Designing an app that has two states

The reliable pattern is to treat "connected" as a feature rather than an assumption. Three habits cover most of it.

Render before you fetch. Draw the layout, the navigation and any static content immediately, then load data into it. An app that waits for a database response before painting anything is an app that shows nothing offline, service worker or not.

Say which state you are in. `navigator.onLine` plus the `online` and `offline` events is enough to put an honest banner at the top of the screen. Users tolerate "no connection — showing your last saved view" far better than a spinner that never resolves.

Fail per feature, not per app. One failed call should disable one panel, not blank the page.

```javascript
async function loadTasks() {
  const tasks = await window.NorthernGoDB.get('tasks');
  render(tasks || []);
  const status = window.NorthernGo.offlineStatus ? window.NorthernGo.offlineStatus() : {};
  if (!status.online) setBanner('No connection — showing your last saved view.');
  else if (status.pending) setBanner(status.pending + ' change(s) waiting to sync.');
  else setBanner(null);
}

window.addEventListener('online', loadTasks);
window.addEventListener('offline', () => setBanner('You are offline.'));
window.addEventListener('ng-offline-queue', loadTasks);
```

### The trade-off of never showing a blank screen

Because page navigation is cache-first, a cached page wins over a newer one. That is exactly what you want in airplane mode and exactly what you do not want ten minutes after you republished the app: a returning visitor can keep seeing the previous version until the cached entry is replaced.

The honest workaround is a hard reload, or clearing the site data for that host, which most people will not think to do. If you are iterating quickly on a live app, test it in a private window so you are not fooled by your own cache, and tell early users to reload once after you ship a change. This is a real limitation rather than a bug: any cache-first strategy trades freshness for the guarantee that the app always opens.

### Icons and installability

The manifest is generated per project, using the app's name and either an icon you uploaded or a generated square with the app's first letter. It sets `display: standalone`, so once installed the app opens without browser chrome and looks like a native app in the task switcher.

A generated letter icon is fine for testing and obvious on a real home screen. If the app is going in front of anyone else, [make proper app icons](/resources/create-pwa-app-icons/) and upload one — it is the cheapest quality signal a PWA has.

### The exported ZIP does not carry the same service worker

Worth knowing before you plan around it. The **Download ZIP** export is a Capacitor-shaped project: your app as `www/index.html`, a manifest, a config file and a `package.json`. It does include a `www/sw.js`, but it is a minimal one that only forwards requests to the network and returns a short offline message when that fails. It does not precache anything.

So the offline behaviour described on this page belongs to the hosted version, not to the export. If you take the ZIP and [wrap it as a native iOS or Android app](/resources/export-native-ios-android-capacitor/), the app shell is bundled locally anyway and the service worker matters much less. If you take the ZIP to your own web hosting, plan on writing a caching service worker yourself.

### Testing it properly

Open the app online once. Then switch the device to airplane mode and reload. You should see the app render normally, not a white screen. In desktop Chrome you can do the same via DevTools with the Network tab set to Offline, which also lets you confirm under Application → Service Workers that one is registered and active.

Two extra checks are worth the minute they take. Under Application → Cache Storage you can see exactly which URLs were stored, which immediately explains any unstyled render. And Application → Manifest will tell you whether the icon and name resolved, which is the usual cause of an install prompt that never appears.

### Troubleshooting

**The app renders as unformatted text offline.** The CDN stylesheets were not in the cache when the connection dropped. They are precached on install, but a CDN failure during that first visit is allowed to pass silently so the install still succeeds. Load the app online once more and check Cache Storage.

**Nothing is cached and there is no service worker.** Almost always the app is being opened through the `?app=` share link rather than its own subdomain or domain. Check the address bar first.

**Changes do not appear after republishing.** Cache-first navigation is serving the old page. Hard reload, or clear site data for that host.

**The app opens offline but shows no data.** That is working as intended. Database calls need a connection. If the screen is empty rather than degraded, the render path is waiting on data instead of drawing first and filling in after.

## Frequently asked questions

### Does the app work offline the very first time it is opened?

No. The app has to be opened once with a connection so the service worker can install and cache the files. After that first visit it opens offline. This is a limitation of how browsers work, not of any particular platform.

### Can the database work offline too?

No. Data that lives in a cloud database requires a connection, and serving cached database responses would show users stale data without warning. Design the app to render from local defaults and sync in the background, so only the live-data parts are affected when the connection drops.

### Is an installable PWA the same as an offline app?

No, and this trips up a lot of people. A manifest file makes an app installable to the home screen, but offline behaviour depends entirely on the service worker and what it caches. Many installable PWAs still show a blank screen without a connection.

### Does the offline cache work on the free plan share link?

No. The service worker and manifest are only served on the app’s own host, meaning a northerngo.com subdomain on Premium or Pro, or a Pro custom domain. The northerngo.com/?app= share link used by free plan projects gets neither, so those apps have no offline behaviour at all.

### Why does my published app still show the old version after I update it?

Because page navigation is served from the cache before the network is consulted. That is what prevents a blank screen offline, but it also means a returning visitor keeps the previously cached page until it is replaced. A hard reload or clearing site data for that address fixes it, and testing in a private window avoids fooling yourself during development.

---

## Related

- [How to turn an AI-built app into an installable PWA with your own icon](https://northerngo.com/resources/create-pwa-app-icons/)
- [How to store data in an AI-built app with Supabase](https://northerngo.com/resources/connect-supabase/)
- [How to connect your own domain to an AI-built app](https://northerngo.com/resources/connect-custom-domain-ssl/)
- [How to export an AI-built app as a native iOS and Android project with Capacitor](https://northerngo.com/resources/export-native-ios-android-capacitor/)

---

NorthernGo is an AI-powered platform for building production-ready web apps with zero coding. Local AI generation via WebGPU is unlimited and free, and you own all generated source code. https://northerngo.com/
