Compatibility

Does Obfuscation Break Your Web App Manifest?

A web app manifest is a contract with the browser written in property names the browser chose. If yours is a static file in the public directory, nothing in this page applies to it. If it is generated, and increasingly it is, by a build plugin, a server route or a framework that emits it from a configuration object, then those keys pass through a JavaScript build on their way to the user agent, and a step that rewrites property names sits between your app and its own identity.

What was measured

One file that builds a manifest the way a generator does, writes it as JSON, and hands the parsed document to a user-agent stand-in copied in unprotected. That reader applies the installability rules browsers apply: a name or short name, a start URL, an app-like display mode and an icon at 192 or 512 pixels. It also registers a service worker with a scope, an update policy and a module type, and reads the shortcut list a browser turns into a jump list.

The base column is clean. All five protection profiles emitted the same document and produced the same verdicts. Protecting a build that generates a manifest does not disturb the manifest.

Everything below is the member-renaming column, and the document itself is printed in every run, because the document is what crosses the boundary. Its top-level key list is printed alongside, which is the fastest way to see the failure for what it is.

A valid file that describes nothing installable

The start URL arm is the clearest. The published document is still valid JSON, still served with the right content type, still linked from the page. One key in it reads as a generated identifier, and the browser side reports the app as not installable with the reason recorded as a missing start URL. The launch URL fell back to the site root, so even where an install had already happened, the app opens somewhere other than where it was designed to open.

Renaming name and short name produced the same verdict with a different symptom: the install prompt had nothing to display and fell back to the placeholder label. An install banner offering an untitled application is a conversion problem long before anybody calls it a bug.

The display arm made the app open in a browser tab rather than a standalone window, because the reader could not find an app-like display mode. The theme colour arm reverted the operating system chrome to the browser default. Neither threw, and neither changed a single visible line of the source.

Icons failed in two different ways, which is worth separating. Renaming the icons array itself left the browser with no icons at all. Renaming the fields inside each icon entry left the array populated but the entries unusable: the count stayed at two while the maskable count fell to zero and the installability check failed on icon size. The second one is the harder to spot, because the document still looks like it has icons.

The service worker half is quieter

The registration call takes an options dictionary, and the two names in it that matter are the scope and the update policy. Renaming them left the registration succeeding, which is the point: nothing threw, and the worker installed.

The update policy reverted to the browser default, which allows the worker script itself to be served from the HTTP cache. A team that set it deliberately did so to guarantee that a new worker is fetched from the network on every check, which is how a bad release is recalled. Losing it does not break anything today; it lengthens the tail of the next incident.

The scope key in the manifest moved in the same arm. Scope decides which pages the installed app treats as its own, so a renamed scope key means the browser derives one instead, and the boundary between the installed application and the rest of the site is no longer the boundary you wrote down.

The arm that looked inert until the reader was asked

The shortcut list measured identical at first. That looked like good news and was not: the reader in the sample simply did not read shortcuts, so the arm proved nothing. This is a standing rule in this series, and it earned its place again. When an arm looks inert, run the operation the renamed name governs rather than concluding it changed nothing.

With the reader extended to build the jump list a browser shows on a long press or a taskbar right-click, the same arm reports the list as empty while the application still prints its own shortcut URLs correctly. The feature is gone on the platform and present in the code, and the only place the difference shows is a context menu nobody tests.

The good news, and it is unusual for this series

Most contracts in this series are invisible to an ordinary presence check, because the check compares strings against keys and neither side moved. Here it is the opposite, and it works in your favour.

The build's own preflight in this file walks a list of required key names and reports which are absent from the manifest object. In every arm above it fired correctly, naming exactly the keys that had been renamed. The reason is structural: the manifest's keys are the renamed side, and the required list is strings, so the comparison genuinely detects the difference.

That makes this one of the few areas where a five-line build check is a complete defence. Run it after generation, on the parsed JSON you are about to write, and fail the build. Do not run it on the object before serialisation in the same module that built it, and do not check only that the file parses, because it parses perfectly in every failing arm here.

What to do about it

If your manifest is a static asset, this is not your problem, and moving it to a static asset is a legitimate fix. The keys are fixed by a specification, they rarely change, and there is little value in computing them at build time.

If it is generated, keep the specification's vocabulary out of any member pattern: name, short_name, start_url, scope, display, theme_color, background_color, icons, src, sizes, type, purpose, shortcuts, and the registration options scope, updateViaCache and type. Anchoring the pattern to your own naming convention covers all of them at once and covers the next key the specification adds.

Then validate the emitted file. Parse the JSON your build wrote, assert the keys the browser needs, and assert one value inside the icons array so that the second failure mode is covered too. A protected build that publishes an unreadable manifest is silent everywhere except in install rates, and install rates move slowly enough that nobody connects them to a release.

Frequently asked questions

Does obfuscation break a progressive web app?

Not in the default configuration. A build that generates a manifest, publishes it and has a user-agent stand-in read it produced identical results on all five protection profiles measured. It becomes a surface only when member renaming reaches the key names the manifest specification fixes, and only when the manifest is generated in JavaScript rather than shipped as a static file.

What happens to start_url if member renaming matches it?

The published document keeps the value under a generated key, so the browser cannot find it. In the measured run the app was reported as not installable with a missing start URL, and the launch URL fell back to the site root, which means an already-installed app opens on the wrong page.

Why does my install prompt say the app has no name?

Because a renamed name or short_name key leaves the browser with nothing to display, so it falls back to a placeholder. The document is still valid JSON and the values are still in it under generated identifiers, which is why a quick look at the file does not reveal the problem.

Can renaming break icons without emptying the icons array?

Yes, and both were measured. Renaming the array leaves the app with no icons. Renaming the fields inside each entry leaves the array populated with entries the browser cannot use: the count stayed at two while the maskable count fell to zero and the installability check failed on icon size.

Does service worker registration still work after renaming?

The registration succeeds, which is what makes it quiet. The update policy reverts to the browser default, so the worker script may be served from the HTTP cache rather than fetched fresh, and a renamed scope key means the browser derives a scope instead of using the one you wrote.

Will a build check catch this?

Here, yes, and that is unusual. Because the manifest keys are the renamed side and a required-key list is made of strings, a presence check genuinely detects the difference. In every failing arm the preflight named exactly the renamed keys. Run it on the parsed JSON you are about to publish.

Should we just ship a static manifest file?

It is a reasonable fix. The keys are fixed by a specification and change rarely, so generating them at build time buys little. If the manifest must be generated, keep the specification vocabulary out of the member pattern and validate the emitted file before it is published.

Related reading