Offline-first and subscriptions in a PWA
Rodney Dela Cruz
Technical Co-founder, eSerbisyo
- progressive-web-apps
- infrastructure
Three problems show up in every installed web app that has to work without a network and has paying users: the app must open cold with no connection, the session must not leak to the wrong person, and the subscription state must be honoured honestly when it cannot be verified.
They interact, which is what makes them hard. None of them has a clever solution.
I co-built FacadeLab, an engineering calculation tool delivered as a PWA. The platform layer — authentication, subscriptions, the local data layer, deployment, and offline support — was my responsibility; the calculators and the engineering standards (ADM 2015, ASTM, ASCE, ACI, AISC) were authored by a collaborator. The decisions below reflect that platform work, not the calculation methods.
The cold start with no network
A user taps the icon on their home screen. The app launches with no network, and possibly days since the last successful request.
Everything the app shows at that moment came from a cache, and the cache is a copy of data that may be wrong. The design question is not “can we cache” — it is which parts are safe to show stale, and which require a fresh answer before they can be displayed at all.
The workable split:
- Cacheable and useful stale: drafts, previous results, reference data, anything the user was already looking at.
- Requires verification: anything showing someone’s private data, anything gated behind a subscription, anything that commits an action.
A calculation tool is unusual in that most of it is genuinely cacheable — the formulas do not change on the server — which is why this is tractable at all. The hard cases are the ones that grant or withhold access.
Auth that survives a dead connection
This is the failure that actually matters, and it is specific: the app launches cold, cannot reach the server, and has to decide who it is showing data to.
The bad outcomes are worse than being logged out:
- Showing one user’s cached private data to whoever opens the app next.
- Rendering a signed-in shell with empty data, which reads as data loss.
- Silently downgrading to a stale role, so someone has fewer permissions than their revocation intended.
The rules that hold up:
- A stale session is unverified. It can render cached public content. It cannot render private content without either re-authenticating or the user explicitly accepting that they are seeing stale data.
- Local data is bound to the session that fetched it. Clearing a session clears the cache with it. Leaving another user’s results on the device is not a bug you can fix later.
- Prefer re-authentication over guessing. Asking for a password is an inconvenience. Showing the wrong person’s data is a breach.
Also, tokens should be validated only when possible. Do not assume validity by age. The absence of a network response must not be treated as confirmation that a revoked token is still good.
Subscriptions you cannot check
Online, entitlements are a lookup. Offline, you are holding a promise you cannot verify, and the client has to decide whether to honour it.
The temptation is to cache the last known state and trust it. That produces the worst outcome available: a revoked subscription keeps working until the next sync, and the person it keeps working for is the one who paid for it.
The honest policy, which is mostly about being explicit rather than clever:
- Unverified is not the same as yes. Treat a subscription you cannot confirm as unconfirmed. Let the user keep working, gate the irreversible or expensive actions, and say why.
- Never cache an entitlement you cannot invalidate. The effective rule is time-boxed: if the last verification is older than a reasonable window, treat premium access as degraded.
- Say which state the user is in. “Your Pro features are active but not verified until you reconnect” is a sentence worth writing. A user who must invent that inference will not do so correctly.
- Downgrade safely. When a subscription cannot be confirmed and would cross an action boundary, block the action. Non-blocking previews can remain, but exports, payments, or anything that cannot be undone should wait.
An installed PWA has an advantage: service workers can hold a cached response, but they cannot guarantee recency. The client must own the recency decision.
Service workers and caching strategy
One caching strategy never fits all. For a calculation tool, static assets should be versioned and cached aggressively so the shell opens cold. API responses are different: lists of calculations, user results, and entitlement checks should not be treated identically.
- Precache the shell and critical assets. This is what makes a cold start work with no network.
- Network-first for entitlements. When online, always fetch the latest subscription state. When offline, fall back to the last verified copy, but mark it as unverified.
- Stale-while-revalidate for read-mostly data. Show cached data immediately, then update in the background if possible. This is useful for reference data that changes rarely.
- Cache only what you can bind to a session. If you cache private results, they must be keyed by user ID and cleared with logout.
Reconnecting and reconciliation
When connectivity returns, a few things must happen in order:
- Re-verify identity and entitlements. Do not just sync writes. Check that the session is still valid and that the subscription state has not changed.
- Replay pending writes idempotently. Each queued mutation needs a client key so the server can ignore duplicates if a response was lost.
- Resolve conflicts. If two devices edited the same draft, surface the conflict explicitly. Avoid last-write-wins for anything the user cannot safely lose.
- Tell the user what changed. A short summary (“2 drafts synced, 1 Pro feature verified”) is far more useful than a silent toast.
Background sync can help, but it is not universal. Treat it as an optimisation, not a contract. The app must still work if background sync never runs.
The parts that are genuinely hard
- Gating something irreversible offline. If the paid action creates a real artefact — a document someone will rely on — you cannot un-create it when the subscription turns out to have lapsed. Either the artefact is provisional, or the action waits for verification.
- Storage eviction. The browser will evict cached data under pressure, and the app has to survive discovering that its cache is gone mid-session.
- Installed state vs browser. Permissions, storage quotas, and background behaviour differ between a PWA installed to home screen and a tab. The cold start must not assume the better case.
- Keeping the local source of truth small. IndexedDB is the right tool, but it is easy to accumulate data you never expire. Define a retention policy, and test cold starts with a nearly-full quota.
What this adds up to
The goal is not “works offline”. It is that every state the app can be in is one you would be willing to explain to a user — including the ones where the answer is “I need you to reconnect” and “I am showing you yesterday’s data.”
An installed app on a dead connection is an ordinary Tuesday for some people. Building for it is mostly the discipline of not pretending to know things you cannot check. For a subscription product in particular, honesty about verification is a feature, not a compromise.