# Placeholder icons kept a PWA uninstallable

> A dev.to report traces a web app that never became installable to two 68-byte placeholder icon files sitting behind a manifest that declared full-size icons.

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](https://dev.to/thekilteddev/never-installable-2fh4)).

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](https://dev.to/thekilteddev/never-installable-2fh4)).

## 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](https://dev.to/thekilteddev/never-installable-2fh4)).

### 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](https://dev.to/thekilteddev/never-installable-2fh4)).

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](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/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](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/icons)).

### 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](https://dev.to/thekilteddev/never-installable-2fh4)).

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](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/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](https://dev.to/thekilteddev/never-installable-2fh4)).

### 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](https://dev.to/thekilteddev/never-installable-2fh4)).

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](https://dev.to/thekilteddev/never-installable-2fh4)).

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](https://developer.mozilla.org/en-US/docs/Glossary/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](https://dev.to/thekilteddev/never-installable-2fh4)).

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()](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share)).

## 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](https://dev.to/thekilteddev/never-installable-2fh4)).

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](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable)),
and the `icons` reference states that for raster formats like PNG the exact available
sizes should be specified
([MDN, icons](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/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](https://dev.to/thekilteddev/never-installable-2fh4)).

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](https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation)).

## Related entries on this site

- [Capabilities](/reference/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](/reference/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](/reference/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

- Which Chrome and Android versions the missing install option was observed on is
  not confirmed.
- Whether Chrome's installability check compares declared `sizes` against 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

- [Never installable](https://dev.to/thekilteddev/never-installable-2fh4) (dev.to)
- [Making PWAs installable](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable) (developer.mozilla.org)
- [icons](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/icons) (developer.mozilla.org)
- [Transient activation](https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation) (developer.mozilla.org)
- [Navigator: share()](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share) (developer.mozilla.org)