Skip to content

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).

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).

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).

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.

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).

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).

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).

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 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).

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).

  • 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.
  • 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.