Offline Support and Write Queue (Android)
What your web app gets for free when MobiWeb's offline support is enabled, what it must do to keep it safe, and the optional AndroidOffline bridge for writes made while offline.
Offline support is a build-time option of the Android app (OFFLINE_ENABLED). When it is off, nothing on this page applies and AndroidOffline is not injected. iOS has no offline support yet (no AppleOffline). Setup is in Offline Support; design, limits and the native side are in the Technical Design.
What happens without any code change
MobiWeb registers a service worker on your origin (/__mobiweb_offline_sw.js, served by the app, not by your server) and keeps a network-first cache of GET responses:
Request | Behaviour |
|---|---|
Same-origin | Network first; the last |
Cross-origin | Same, incl. opaque ( |
Cross-origin API calls, any non- | Untouched. Never cached. |
Anything with | Never cached. This is your per-endpoint opt-out. One exception: your app shell (the start page or |
Redirected responses, non- | Never cached. |
Navigations while offline fall back, in order, to: the exact URL → the app's start URL → the last document that loaded successfully → /. Your client-side router therefore boots on any deep link with the shell document and must handle that route itself. A navigation that takes longer than 8 s on a bad connection is also served from the cache (and the late response is still stored for next time).
On the first page load after the service worker is installed, MobiWeb warms the cache with the current page, its same-origin page assets, and the start URL, so one online session (logged in) is enough for an offline cold start of the pages the user visited.
Things to check on your side:
Same-origin API. Only same-origin API responses are cached. If your API lives on another host, only the shell works offline.
Response headers. Frameworks like Spring Security send
Cache-Control: no-storeon every authenticated response by default. (no-storeon the start page HTML itself is fine: the shell exception above covers it. Other HTML pages markedno-storeare not cached; deep links into them boot from the shell.) MobiWeb honors it, so such endpoints are never available offline. Decide per endpoint.Your own service worker. If the page registers one (any scope, including a push SW), it is respected: MobiWeb's is not registered, an already-installed MobiWeb worker is unregistered and its cache deleted, and your worker's network behaviour is not altered. Only the browser's HTTP cache remains as an offline fallback for the page. If you register your SW late, MobiWeb's may control the very first session; it is replaced or removed by the next load.
CSP.
worker-src 'self'(orscript-src 'self') must be allowed for/__mobiweb_offline_sw.js.Code splitting. Lazy chunks of routes the user never visited are not cached; offline, such a route renders the shell and then hits your chunk-load error path. Keep the previous build's assets on the server for a while, as for any SPA deploy.
Token auth. Cookie sessions work offline as is. If you validate or refresh a bearer token on boot with a
POSTor ano-storeGET, that call fails offline; don't log the user out on a network error.Logout (required). Call
AndroidCookie.clearCookies(...)(or use the configured redirect-logout URL). Both also wipe the offline cache and the write queue. A logout that only doesPOST /logout+localStorage.clear()leaves the previous user's cached API responses servable offline to the next user of a shared device. As a minimum, callAndroidOffline.clearCache()andAndroidOffline.clear().
Detecting whether MobiWeb's worker is active:
Connectivity event
Event: mobiweb:connectivity-> { online: boolean }
Fired on every change of the device's default network (Android system connectivity, not reachability of your server). navigator.onLine keeps working too.
Write queue
Writes made while offline are not replayed automatically: MobiWeb can't know what a failed POST means to your app. Queue them yourself through the bridge; MobiWeb persists them (encrypted, surviving restarts) and replays them with the user's cookies when the device is back online.
Bridge object: AndroidOffline
AndroidOffline.enqueue(requestJson)
Field | Type | Description |
|---|---|---|
|
| Absolute or relative; resolved against the app's start URL. Must be on the app origin (same scheme, host and port). |
|
|
|
|
| Optional. |
|
| Optional. Text only (JSON, form-encoded…), max 1 MB. No binary or multipart. |
|
| Optional, default |
|
| Optional JSON, max 4 KB. Stored and echoed unchanged in the item's events: use it for your local record id. |
Returns a JSON string: {"ok":true,"id":"…"} or {"ok":false,"error":"…"}. Errors include a non-app origin, an unsupported method, oversized body/meta, and Queue full (max 500 items). Only accepted while the app page itself is loaded in the main frame.
Replay rules:
Strict FIFO, one item at a time.
2xx→ item removed →mobiweb:offlineQueueItemSent.3xxor4xx→ item removed →mobiweb:offlineQueueItemFailed. Redirects are not followed: a302to the login page means the session expired and the write was not applied.5xx→ kept, counts as an attempt, replay stops and retries with backoff (5 s doubling to 5 min).No response (network error) → kept, not counted, same backoff.
maxAttemptsreached → removed →mobiweb:offlineQueueItemFailed.Replay runs automatically when connectivity returns and on the first app page load after the app starts. Results for items replayed while no app page was loaded are delivered as soon as one is.
Replays carry the WebView's cookies and send
Origin/Refereras the app origin andX-Requested-Withas the app's package name, like a normal in-app request.
AndroidOffline.list()
Returns a JSON string: array of queued items { id, url, method, headers, body?, meta?, attempts, maxAttempts, createdAt }, oldest first. Use it on boot to show pending work.
AndroidOffline.remove(id)
Returns boolean. Drops a queued item; if it is being sent at that moment, the request is aborted.
AndroidOffline.flush()
Replays now, cutting any pending backoff short. Normally not needed.
AndroidOffline.clear()/AndroidOffline.clearCache()
Empty the write queue / delete the offline response cache. Both also happen on AndroidCookie.clearCookies(...).
AndroidOffline.isOnline()/AndroidOffline.getStatus()
isOnline() returns boolean. getStatus() returns a JSON string { online: boolean, serviceWorkerConflict: boolean, queueLength: number }; serviceWorkerConflict is true when your own service worker prevented MobiWeb's from registering.
Events
Event | Detail | When |
|---|---|---|
|
| Server answered |
|
|
|
|
| A replay pass ended (queue empty, offline again, or waiting for backoff). |
Example
Design your queued writes to be idempotent where possible (e.g. a client-generated id in the body): a request can be retried after a 5xx or a connection drop, and an item whose result could not be recorded locally may be sent again after the app process is restarted.
Testing from the web side
Build the app with
OFFLINE_ENABLED=trueand openchrome://inspecton a desktop Chrome.Application → Service Workers:
/__mobiweb_offline_sw.jsmust be active for your origin; Cache Storage →mobiweb-offline-v1fills while you browse.Airplane mode, then cold-start the app: the pages and API data visited before must render; endpoints you marked
no-storemust not.In the console:
AndroidOffline.enqueue(JSON.stringify({url: '/api/ping', method: 'POST', body: '{}'}))while offline, then disable airplane mode and watch themobiweb:offlineQueue*events.