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:
- A custom domain (Pro). The app is the whole site at that address, so
/sw.jsand/manifest.jsonresolve 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 if you are on Pro.
What gets cached
Three things are stored on the device during that first visit:
- The app shell — the HTML page itself, so navigation offline serves the cached copy instead of failing.
- Styling and icon resources — the Tailwind and icon CSS the app loads from a CDN, so it renders styled rather than as unformatted text.
- 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. OnlyGETrequests are eligible for caching in the first place, so everyPOSTyour 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.
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.