Placeholder icons kept a PWA uninstallable
Published
On 2026-09-19 the dev.to account thekilteddev published a report titled “Never installable” about a proof-of-concept food-logging web app built by one technical person working alongside an NHS dietetic team, and the finding it leads with is that the app had never once been installable on a real phone. The report states that the manifest declared full-size icons while the two actual icon files behind those declarations were 68-byte placeholder stubs, one pixel by one pixel, and that the service worker, the manifest, and the caching logic underneath all of it had been correct from the start (the report).
The report frames the three days before that discovery in the same terms: the app worked, or seemed to, every test passed, every review came back clean, and nothing had ever said that installation was not available. It is written as a debugging log rather than a product announcement, and the bugs it logs are the kind that only a physical device produces (the report).
What the report traces
Section titled “What the report traces”The report works outward from the symptom to the file, and each step it records is taken in turn below, in the terms the report and the documentation it concerns actually use. Three threads run through it: the icon declaration, the two wrong theories that preceded it, and a set of export failures that surfaced in the same session (the report).
The two placeholder files
Section titled “The two placeholder files”The report’s account of the icon bug is specific about what Chrome does with a declaration it cannot satisfy. It states that Chrome checks that a PWA’s declared icons actually exist at the sizes it claims, that when they do not, nothing in the console warns about it and no error ever fires, and that the “Install app” option simply does not appear. It adds that “Add to Home Screen” still worked, but could only ever produce a bookmark shortcut rather than a real installed app, so offline never worked either (the report).
MDN documents the manifest side of that check. It states that Chromium-based
browsers, including Google Chrome, Samsung Internet, and Microsoft Edge, require
that the manifest includes name or short_name, a start_url, display and/or
display_override, prefer_related_applications set to false or absent, and an
icons member that must contain a 192px and a 512px icon. It also states that
while service workers are not a requirement for a PWA to be installable, many PWAs
use them to provide an offline experience, which is the order of dependency the
report ran into from the other end
(MDN, Making PWAs installable).
The icons reference adds the detail that makes a declared size load-bearing. MDN
states that for raster formats like PNG, specifying the exact available sizes is
recommended, and that for vector formats like SVG, any can be used to indicate
scalability. A sizes value is therefore a claim about the bytes on disk, and the
report’s two files were the same claim with nothing behind it
(MDN, icons).
The theories that came first
Section titled “The theories that came first”Before anyone opened the icon files, the report states that two other explanations took the blame. The first was the browser: that DuckDuckGo’s PWA support might be incomplete. The second was the host: that something about how Namecheap served the files was the difference from the version that had worked on Netlify. The report states that both theories were plausible and both were wrong, and that an earlier comparison between the two hosts had already been written up as “decisive, host-specific” (the report).
The browser theory was plausible for a documented reason. MDN states that on Android the only browsers that install PWAs as WebAPKs are Chrome on devices with Google Mobile Services and Samsung Internet on Samsung devices, and that Firefox, Edge, Opera, and other browsers, including Chrome on devices where Google Mobile Services is not present, instead add a browser-badged home-screen shortcut that opens the site in the browser. A home-screen entry that behaves like a bookmark is thus an expected outcome on several Android browsers, independent of any icon bug, which is exactly what makes it a poor signal to debug against (MDN, Making PWAs installable).
The report’s resolution of the host theory is that the real difference was cache temperature rather than hosting. It states that Netlify’s service-worker cache had been warm for months while Namecheap’s was cold, that the warm cache had quietly been hiding two unrelated bugs the whole time, and that what surfaced all three bugs at once was a fresh cache forcing every code path to run for real, on a real phone, for the first time (the report).
The export path, found on a phone
Section titled “The export path, found on a phone”The report gives four export bugs and attributes all four to real-device testing.
The first was a silent no-op when someone tapped “Download” on an Android phone in
DuckDuckGo, because a CDN script, xlsx.full.min.js, that the service worker had
not cached could fail to load without throwing anything visible. The fix for that
introduced the second: the downloaded file came back as .bin instead of .xlsx,
because the fallback download mechanism was not reliably setting the file’s MIME
type on that browser, which the report fixed by building the download from an
explicit Blob
(the report).
The third is the one with a platform rule behind it. The report states that the app reported “Downloaded” with nothing actually appearing, because the click handler waited on three IndexedDB round-trips before triggering the download, and that wait was long enough for the phone to quietly expire the tap’s permission to trigger one at all. Its fix was to pre-build the export the moment the screen opened, so the tap had almost nothing left to wait on (the report).
MDN’s definition of transient activation describes the state that expired. It
states that transient activation is a window state indicating that a user has
recently directly and meaningfully interacted with the window, that the state is
enabled following interactions such as a touchend event, that it expires after a
timeout if not renewed by further interaction, and that it may also be consumed or
deactivated after using some gated features. It also states that transient
activation is commonly used as a mechanism for ensuring that a web API can only
function if triggered by user interaction
(MDN, Transient activation).
The fourth bug the report assigns to the platform rather than the app. It states that sharing the file to WhatsApp on Chrome for Android threw a permission error even though the browser’s own API had said sharing was possible, and that an A/B test settled why: a plain text file shared fine, while the identical share swapped to the real xlsx MIME type failed every time. The report’s reading is that Android Chrome’s own share implementation rejects the file type at the native layer, and that the fix is a fallback rather than a workaround, catching that specific failure and dropping back to the working download path (the report).
MDN’s share() reference carries both halves of that shape. It states that the
promise may be rejected with NotAllowedError when a file share is being blocked
due to security considerations, and with TypeError when files are specified but
the implementation does not support file sharing. Under its list of usually
shareable file types it gives .txt as text/plain, and it states that you should
always test with navigator.canShare() if sharing would succeed
(MDN, Navigator: share()).
Why it matters for PWA developers
Section titled “Why it matters for PWA developers”The report’s own generalisation is about the absence of feedback rather than about icons. On the install bug it states that nothing in the console warns, that no error ever fires, and that the install option is simply missing, which leaves a developer with a negative to explain and no event to attach a breakpoint to. On the export bugs it states that two of the four gave nothing to go on, and that it was a fresh cache on a real phone, not a test suite, that forced every code path to run (the report).
The documented requirements explain why a green build said so little here. MDN
lists the manifest members Chromium-based browsers require, including a 192px and a
512px icon
(MDN, Making PWAs installable),
and the icons reference states that for raster formats like PNG the exact available
sizes should be specified
(MDN, icons).
The report states that Chrome checks that the declared icons actually exist at the
sizes claimed, that nothing warns when they do not, and that its own rule is to
verify the install flow on the device that performs it
(the report).
The transient-activation case generalises further, because it has nothing to do with manifests. MDN states that the activation state expires after a timeout if not renewed, and that it gates whether an API can function at all. Any handler that awaits storage, network, or worker round-trips before calling a gated API is therefore racing that timeout, and the report’s fix, moving the expensive work ahead of the tap, is the shape that follows from the definition (MDN, Transient activation).
Related entries on this site
Section titled “Related entries on this site”- Capabilities: follow this cross-reference alongside the report’s install-prompt failure and the manifest members MDN lists as required for a Chromium-based browser to promote a web app for installation.
- Performance: the report’s third export bug is a latency budget rather than a logic error, with three IndexedDB round-trips outlasting the tap’s transient activation; follow this cross-reference alongside that detail.
- Storage: follow this cross-reference alongside the report’s account of a warm service-worker cache hiding two bugs for months, and of IndexedDB reads sitting on the critical path of a user gesture.
Open and unverified
Section titled “Open and unverified”- Which Chrome and Android versions the missing install option was observed on is not confirmed.
- Whether Chrome’s installability check compares declared
sizesagainst the decoded image dimensions, or only against the file being present and decodable, is not confirmed. - Whether a bundler, a linter, or a manifest schema validator could detect a declared icon size that the file behind it does not satisfy is not confirmed.
- Whether any test harness can simulate the transient-activation timeout the report’s third export bug hit is not confirmed.
- Whether a 68-byte one-pixel PNG would also fail the 192px and 512px requirement if the manifest declared it honestly as a one-pixel icon is not confirmed.
- Whether the three IndexedDB round-trips exceeded a specific transient-activation timeout, and what that timeout is on the device in question, is not confirmed.
- Whether Android Chrome’s rejection of the spreadsheet MIME type is a fixed allowlist, a version-specific behaviour, or a device-level policy is not confirmed.
- Whether the two bugs the report says the warm Netlify cache had been hiding were the export bugs, the icon bug, or some combination is not confirmed.
Sources
Section titled “Sources”- Never installable (dev.to)
- Making PWAs installable (developer.mozilla.org)
- icons (developer.mozilla.org)
- Transient activation (developer.mozilla.org)
- Navigator: share() (developer.mozilla.org)