Receiving shared PDFs in a PWA with no backend
Published
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).
How the share registration is declared
Section titled “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).
What the manifest member requires
Section titled “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).
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).
Why the service worker reads the body
Section titled “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).
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).
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()).
The handoff from the service worker to the page
Section titled “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
Section titled “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).
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).
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).
How the file reaches a tool page
Section titled “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).
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).
The two smaller integrations
Section titled “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).
Manifest shortcuts
Section titled “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).
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).
The iOS install hint
Section titled “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).
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).
Why it matters for PWA developers
Section titled “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).
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).
MDN’s caching guide states that the cache is reachable from both the main thread and the service worker (MDN, 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).
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).
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).
Related entries on this site
Section titled “Related entries on this site”- Capabilities: follow this entry when comparing the support limits cited here for share targets, manifest shortcuts, and the install prompt event.
- Storage: follow this entry while tracing the post’s handoff from Cache to IndexedDB.
- 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
Section titled “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
beforeinstallpromptdoes not fire there cannot be pinned to a release. - Whether the file input receives the
DataTransferobject’sfileslist by assignment, or by some other step, is not stated by the post; MDN documentsfilesas a read-only property ofDataTransfer. - Whether the cache entry keyed
shared-0is 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
Section titled “Sources”- I Made My Web App Receive Files From WhatsApp — No Backend, Just Web APIs (dev.to)
- share_target (developer.mozilla.org)
- shortcuts (developer.mozilla.org)
- Request: formData() method (developer.mozilla.org)
- DataTransfer (developer.mozilla.org)
- Window: beforeinstallprompt event (developer.mozilla.org)
- Caching (developer.mozilla.org)