Distribution Model

Protect JavaScript in a progressive web app

A progressive web app is the delivery model where your entire client is copied onto the device and kept there. It runs when your server is unreachable, which is the feature, and it means the usual advice to move an important decision server-side is unavailable exactly when you would want it.

The Short Version

Offline changes what a control can be

Protection makes the resident copy expensive to read. It cannot make an offline decision trustworthy, because there is nothing to check with. Design for deferred authority rather than local authority.

Whole app residentEvery precached route sits on disk and can be read at leisure
Two entry pointsThe service worker is a separate build input and a separate scope
Guards behave differentlyA worker scope has console, navigator and location; a worklet does not
Order mattersProtect before the precache manifest is generated, never after
The Model

What an installed app changes about the problem

Three differences from an ordinary web page, and each of them changes what a control is worth.

The entire client is resident

Precaching exists so the app opens without the network, which means every route, every module and every asset it needs is written to the device on first visit and stays there. An ordinary page hands out whatever the current visit requires; an installed app hands out the whole thing, once, and the copy persists. Anyone examining it works from local files at their own pace, with no request to your server for you to see, rate-limit or refuse.

Offline removes the server answer

Almost every hard question on this site resolves the same way: move the decision to your server, where the person you are protecting against does not get a vote. An offline-capable app cannot do that at the moment it is offline. This is not a flaw in the model, it is the model. It does mean that a check performed while offline is being performed entirely on hardware the user controls, and no amount of protection changes what that is worth.

The update channel is code you wrote

In a normal site the browser fetches the current version. In a PWA a script you shipped decides which version runs, when a new one is fetched, and whether an old one keeps serving. That makes the service worker the highest-value file in the application: it determines what code runs tomorrow. A stale or wedged one can pin users to an old build for a long time.

The Service Worker

A second entry point, with its own rules

The most common protection mistake in a PWA is treating the service worker as though it were part of the bundle. It is not.

It is a separate build input

The worker is registered by URL and evaluated in its own scope, so a bundler walking your application entry point may never include it and a protection step configured against bundler output may never see it. Nothing fails when it is skipped, which is exactly why it survives. Check the built file directly rather than assuming the pipeline reached it.

The guards do evaluate there

This is where a worker differs usefully from a worklet. A service worker scope provides console, navigator and location, so the domain, browser and OS locks find the objects they inspect and evaluate normally rather than treating absence as failure. Console suppression applies as it does anywhere else. The scope is genuinely more hospitable to runtime options than a worklet is, and the failure mode that catches people out there does not apply here.

Its lifecycle is not your page's

A worker is installed, activated, and then terminated and restarted freely by the browser between events. Anything that assumes continuous execution, including timer-driven checks, is measuring a scope that keeps being torn down. Worse, a guard that throws during install can prevent activation, which does not break the current session but can stop the next version taking over. Test the update path, not only the first load.

Build Order

The precache manifest is generated from your files

This is the ordering mistake that produces a broken deployment rather than a subtle one.

Protect first, generate second

A precache manifest lists the files to cache with a revision derived from their contents. If you protect after the manifest is generated, every revision in it now refers to bytes that no longer exist. Depending on the tooling that yields a failed install, a permanently stale cache, or an app that refuses to update. Protection belongs before the step that fingerprints your output, for the same reason it belongs before your bundler's hashing stage.

Deterministic builds make it checkable

Because output is polymorphic per build by default, two builds of identical source produce different bytes and therefore different revisions. That is usually what you want. When you need to prove that a rebuild produced the same artefact, a fixed seed makes output byte-identical for the same input and options, which is what turns a cache-and-update question into something you can verify rather than assert.

Names the browser calls stay reserved

Event names such as install, activate, fetch, message and push are ordinary string literals passed to an event registration, and string handling preserves values, so they are safe. Members of your own objects that the browser or another context calls by name are the exposure, and they follow the usual reserved-name rules. The web app manifest is JSON describing your app to the browser and is not code at all.

What To Do

Controls that keep working in this model

Given that offline removes server authority, the useful question is what is left.

Defer authority rather than abandoning it

The alternative to deciding offline is not deciding locally, it is deciding later. Let the app work offline against the last known state, record what happened, and reconcile when connectivity returns. That converts an unenforceable local check into an eventual server-side one, which is a real control with a known window rather than a decorative one.

Domain locking fits this model well

An installed PWA runs from your own origin, and location is available in both the window and the worker scope, so a domain lock evaluates properly in both. It is a reasonable fit here in a way it is not in scopes that lack location. As always it constrains where your code runs rather than what it reveals, and the failure action should be chosen deliberately.

Protect the whole client, not just the bundle

Because the entire app is resident, partial coverage is more visible here than elsewhere. The application bundle, the service worker, any workers it spawns and any lazily fetched modules are all on the device together, and an unprotected one sitting beside protected ones is where a reader will start. Confirm coverage against the built output.

Frequently Asked

Questions this page gets asked

Does obfuscation break a progressive web app?

No, and the failures that get attributed to it are usually build ordering or coverage rather than the transform. The two that matter are protecting after your precache manifest is generated, which leaves every revision pointing at bytes that no longer exist, and leaving the service worker out of the input set because your bundler never walked it. Both are visible by inspecting the built output. The transform itself preserves behaviour and a PWA runs it like any other script.

Is my service worker protected along with the rest of my bundle?

Only if it is in your input set, and often it is not. The worker is registered by URL and evaluated in its own scope, so a bundler that walks your application entry point may never see it, and a protection step configured against that bundler's output will not include it. Nothing fails when it is skipped, so it can persist for a long time unnoticed. Open the built worker file and confirm it looks protected rather than assuming the pipeline covered it.

Do the runtime guards work inside a service worker?

Yes, and this is where a worker differs from a worklet. A service worker scope provides console, navigator and location, so the domain, browser and operating-system locks find the objects they inspect and evaluate normally instead of treating a missing object as a lock failure. Console suppression applies too. The caveat is lifecycle rather than scope: the browser terminates and restarts a worker freely, so anything timer-driven is running in something that keeps being torn down, and a guard that throws during install can stop the next version activating.

Should I obfuscate before or after generating the precache manifest?

Before, without exception. The manifest records a revision derived from each file's contents, so protecting afterwards changes the bytes and invalidates every entry. The symptoms range from an install that fails to a cache that never updates. The same rule applies to any step that fingerprints or hashes your output, including your bundler's own content hashing, and it is the same ordering that keeps long-term caching correct.

Can I run a licence or entitlement check in a PWA that works offline?

You can run one, but be clear about what it is worth. While offline the check executes entirely on hardware the user controls, with no way to consult anything they cannot also change, so it is a deterrent rather than an enforcement point. The workable design is deferred authority: operate offline against the last known state, record activity locally, and reconcile with your server when connectivity returns. That gives you a real control with a bounded window instead of a local check that protection cannot make trustworthy.

Does protecting my code affect the web app manifest or installability?

No. The web app manifest is a JSON document describing your application to the browser, covering things like the name, icons and start URL. It is not code, so no transform touches it, and the install criteria a browser applies are unaffected. The same is true of your icons and other static assets. What protection covers is the JavaScript, including the service worker if you remember to include it.

Related Guides

Where to read next

A phased rollout plan

Reproducible before strong, one low-blast-radius bundle first, and runtime guards last and alone.

Next Step

Work out what your offline app can actually enforce

The transform is the straightforward part. The decisions worth making are which checks you defer to your server, how you cover the service worker, and where protection sits in your build order.