# Receiving shared PDFs in a PWA with no backend

> A dev.to build report registers a PDF tools PWA as an Android share target, using the manifest member and service worker fetch handler MDN documents.

On 2026-10-09 the dev.to account Shahzaib published the fifth part of a series about
PdfWord, a PDF tools site the series describes as free and backend-free. The post
reports three platform integrations added to that site, all of which the post states
are client-side with zero backend: registration in the Android share sheet so a PDF
shared from WhatsApp, Gmail, or Files opens pre-loaded in the app, three manifest
shortcuts reachable by long-pressing the installed icon, and an install hint shown to
iOS users
([the post](https://dev.to/shahzaib11/i-made-my-web-app-receive-files-from-whatsapp-no-backend-just-web-apis-37md)).

## How the share registration is declared

The post states that the share integration takes two pieces, and gives the manifest
half first. Its declaration sets `action` to `/share.html`, `method` to `POST`,
`enctype` to `multipart/form-data`, and a `params.files` entry whose `name` is `pdfs`
and whose `accept` list is `application/pdf` and `.pdf`
([the post](https://dev.to/shahzaib11/i-made-my-web-app-receive-files-from-whatsapp-no-backend-just-web-apis-37md)).

### What the manifest member requires

MDN documents each of those keys. It describes `share_target` as the manifest member
that allows installed Progressive Web Apps to be registered as a share target in the
system's share dialog, and states that once registered and installed, a PWA that uses
the Web Share Target API acts as a content sharing target alongside typical system
share targets like email, messengers, and other native apps that can receive shared
content. Of the member's properties, MDN marks `action` and `params` as required, and
describes `method` as the HTTP request method to use, either `GET` or `POST`, with
`POST` to be used if the shared data includes binary data like images, or if it changes
the target app. MDN states that to accept shared files the HTTP method must be `POST`,
the `enctype` must be `multipart/form-data`, and a `files` entry defining the types of
accepted files must be provided, and that files must have a `name` property while the
`accept` property must specify accepted MIME types or file extensions. MDN adds that
defining both is probably a good idea, since operating systems might differ in which
they prefer
([MDN, share_target](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target)).

MDN also records two conditions on the member that the post does not restate. The
first is scope: MDN states that a PWA can only act as a web share target if it has
been installed. The second is support: MDN labels the member "Limited availability"
and states that the feature is not Baseline because it does not work in some of the
most widely-used browsers
([MDN, share_target](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target)).

### Why the service worker reads the body

The second piece in the post is a `fetch` listener in the service worker. The post's
stated reason for putting the parsing there is that a static host cannot parse
multipart forms but the service worker can. Its handler matches on
`e.request.method === 'POST'` together with a pathname of `/share.html`, awaits
`e.request.formData()`, takes the entries under the form field name `pdfs` with
`fd.getAll('pdfs')`, and filters out anything without a positive `size`
([the post](https://dev.to/shahzaib11/i-made-my-web-app-receive-files-from-whatsapp-no-backend-just-web-apis-37md)).

MDN documents the same route for `POST` share data. It states that you can either
handle `POST` share data using server-side code or, to provide a better experience for
offline users, use a `fetch` event listener to intercept the HTTP request, which allows
you to access the data in a service worker. Its own example for that path calls
`event.request.formData()` inside `event.respondWith()`
([MDN, share_target](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target)).

MDN's reference for that call sets out what it returns and how it fails. It describes
`formData()` on the `Request` interface as the method that reads the request body and
returns it as a promise that resolves with a `FormData` object, and lists the reasons a
`TypeError` is thrown: the request body is disturbed or locked, there was an error
decoding the body content, the MIME type of the body cannot be determined from the
`Content-Type` headers included in the request or is not
`application/x-www-form-urlencoded` or `multipart/form-data`, or the body cannot be
parsed as a `FormData` object
([MDN, Request.formData()](https://developer.mozilla.org/en-US/docs/Web/API/Request/formData)).

## The handoff from the service worker to the page

A service worker that has parsed the form still has to get the bytes to a document.

### Where the bytes wait

The post's handler opens a cache it names `pdfword-shared-v1` and calls `cache.put()`
with the key `shared-0`, wrapping the first file in a `Response` whose headers carry an
`X-Filename` value produced by `encodeURIComponent`. It then returns
`Response.redirect('/share.html?shared=1', 303)`
([the post](https://dev.to/shahzaib11/i-made-my-web-app-receive-files-from-whatsapp-no-backend-just-web-apis-37md)).

Both halves of that are documented. On the redirect, MDN states that the `POST`
request is ideally replied to with an HTTP 303 See Other redirect, to avoid multiple
`POST` requests from being submitted if a page refresh was initiated by the user. On
the storage, MDN states that in order to get the files from the service worker context
over to client contexts, one solution is to temporarily write the files to the `Cache`
or IndexedDB and then notify its clients using `Client.postMessage()`
([MDN, share_target](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target)).

MDN's caching guide explains why a cache entry works as a channel between the two
contexts. It describes the `Cache` interface as providing persistent storage for
`Request`/`Response` pairs, with methods to add and delete those pairs and to look up a
cached `Response` matching a given `Request`, and states that the cache is available in
both the main app thread and the service worker, so it is possible for one thread to
add a response there and the other to retrieve it
([MDN, Caching](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Caching)).

### How the file reaches a tool page

The post describes what happens after the redirect. The page at `/share.html` reads
the file from the cache, shows it, and offers tool buttons, of which the post names
Compress, Merge, and PDF to Word. Clicking one stashes the file in IndexedDB and
navigates to the tool page, where a small script in the site's shared `common.js`
injects it into the file input via `DataTransfer`, which the post says means the tool's
existing upload handler just works with no per-tool changes needed. The post's summary
of the result is that the user never picks a file twice
([the post](https://dev.to/shahzaib11/i-made-my-web-app-receive-files-from-whatsapp-no-backend-just-web-apis-37md)).

MDN documents the object that last step names. It describes `DataTransfer` as the
object used to hold any data transferred between contexts, such as a drag and drop
operation or clipboard read and write, states that it may hold one or more data items
each of one or more data types, and lists a `DataTransfer()` constructor that creates
and returns a new `DataTransfer` object. Its read-only `files` property contains a list
of all the local files available on the data transfer. MDN also notes that
`DataTransfer` was primarily designed for the HTML Drag and Drop API and is still
specified in the HTML drag-and-drop section, but is now also used by other APIs
([MDN, DataTransfer](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer)).

## The two smaller integrations

Beside the share registration, the post reports two further additions: a manifest
member, and a UI affordance for a browser that does not fire the event the install
button was built around
([the post](https://dev.to/shahzaib11/i-made-my-web-app-receive-files-from-whatsapp-no-backend-just-web-apis-37md)).

### Manifest shortcuts

The post says it adds three quick actions to the manifest, naming them Merge PDF,
Compress PDF, and PDF to Word. The shown manifest snippet gives Merge PDF a `name`,
a `url`, and a 192x192 icon, then uses an ellipsis for the remaining objects. Its stated
behaviour is that long-pressing the installed app icon jumps straight to the tool, and
its stated cost is five minutes of work
([the post](https://dev.to/shahzaib11/i-made-my-web-app-receive-files-from-whatsapp-no-backend-just-web-apis-37md)).

MDN describes the member as used to specify links to key tasks or pages within your
web application, and states that browsers can use this information to create a context
menu, which is typically displayed when a user interacts with the web app's icon. It
states that shortcuts are usually presented to users in a context menu when they
interact with the web app's icon, such as by right-clicking or long-pressing it, and
that of each object's properties only `name` and `url` are required. MDN qualifies what
the author controls: it states that the presentation and the number of shortcuts shown
to users is at the discretion of browsers and the operating system, and that browsers
may truncate the list of declared shortcuts to remain consistent with the conventions
or limitations of the host operating system
([MDN, shortcuts](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/shortcuts)).

### The iOS install hint

The post states that iOS Safari does not fire `beforeinstallprompt`, so its install
button did nothing there, and that iOS users now get the button too: tapping it shows
text instructing the reader to tap Share and then Add to Home Screen to install
PdfWord. The post's characterisation of the change is that it is small and honest, and
better than a dead button
([the post](https://dev.to/shahzaib11/i-made-my-web-app-receive-files-from-whatsapp-no-backend-just-web-apis-37md)).

MDN describes the event the button was built around. It states that
`beforeinstallprompt` fires when the browser has detected that a website can be
installed as a Progressive Web App, that there is no guaranteed time this event is
fired but it usually happens on page load, and that the typical use is a web app
providing its own in-app UI inviting the user to install the app rather than the
generic one provided by the browser. MDN's own guidance for that pattern is to reveal
the in-app installation UI from the handler, and it gives the reason directly: the UI
should be hidden by default, because not all browsers will support installation
([MDN, beforeinstallprompt event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeinstallprompt_event)).

## Why it matters for PWA developers

The reusable part of the post is the division of labour between a static host and a
service worker. MDN states that accepting shared files requires `POST` with
`multipart/form-data`, and that intercepting the request in a `fetch` handler is the
documented alternative to handling that body with server-side code
([MDN, share_target](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target)).

The handoff is the next documented step. MDN names the `Cache` and IndexedDB
as the places to temporarily write files so client contexts can read them, and it
recommends answering the share request with a 303 redirect
([MDN, share_target](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target)).

MDN's caching guide states that the cache is reachable from both the main thread and
the service worker
([MDN, Caching](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Caching)).

Two caveats attach to the share target itself. MDN states that a PWA can only act as a
web share target once it has been installed, so it is not registered as a share target
before install, and MDN labels `share_target` as not Baseline because it does not
work in some of the most widely-used browsers
([MDN, share_target](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target)).

The install button carries a documented caveat of its own. MDN's reason for hiding
in-app install UI by default is that not all browsers will support installation
([MDN, beforeinstallprompt event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeinstallprompt_event)).

Finally, MDN attaches a security note to the whole mechanism that the post does not
discuss: similar to HTML form submissions, you should be cautious about data sent to
your application via the share target, and be sure to validate incoming data before
using it — which covers any handler that writes a shared file into a cache
([MDN, share_target](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target)).

## Related entries on this site

- [Capabilities](/reference/capabilities/): follow this entry when comparing the
  support limits cited here for share targets, manifest shortcuts, and the install
  prompt event.
- [Storage](/reference/storage/): follow this entry while tracing the post's handoff
  from Cache to IndexedDB.
- [Performance](/reference/performance/): follow this entry when evaluating the post's
  stated goal of opening the shared file pre-loaded rather than asking the user to
  pick it twice.

## Open and unverified

- Which Android browser and version the share sheet behaviour was observed in is not
  stated by the post, so the claim cannot be pinned to an engine.
- Which iOS version the install hint was tested against is not stated, so the post's
  report that `beforeinstallprompt` does not fire there cannot be pinned to a release.
- Whether the file input receives the `DataTransfer` object's `files` list by
  assignment, or by some other step, is not stated by the post; MDN documents `files`
  as a read-only property of `DataTransfer`.
- Whether the cache entry keyed `shared-0` is deleted after the file is consumed is not
  addressed, so what a second share does to a pending one is not confirmed.
- Whether more than one shared file is handled is not confirmed: the post's filter
  collects every entry with a positive size but its `cache.put()` stores only the first.
- Whether the shared file is validated before use, which MDN advises for share target
  input, is not addressed by the post.

## Sources

- [I Made My Web App Receive Files From WhatsApp — No Backend, Just Web APIs](https://dev.to/shahzaib11/i-made-my-web-app-receive-files-from-whatsapp-no-backend-just-web-apis-37md) (dev.to)
- [share_target](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target) (developer.mozilla.org)
- [shortcuts](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/shortcuts) (developer.mozilla.org)
- [Request: formData() method](https://developer.mozilla.org/en-US/docs/Web/API/Request/formData) (developer.mozilla.org)
- [DataTransfer](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer) (developer.mozilla.org)
- [Window: beforeinstallprompt event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeinstallprompt_event) (developer.mozilla.org)
- [Caching](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Caching) (developer.mozilla.org)