Offline & PWA · 9 min read · Updated 15 August 2026

How to build a web app that still works without internet

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:

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 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.

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.

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 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, 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.

Build it yourself

NorthernGo turns a plain-text description into a working web app with a database, login and a live URL. Local AI generation runs on your own GPU, is unlimited, and is free on every plan.

Start building free