File Handling Protocol Handlers

File Handling lets your PWA register as the default opener for custom file types -- double-click a .recipe and your app launches with it. Protocol Handlers do the same for custom URL schemes like web+myapp://

June 12, 20264 min read3 / 6

When a user double-clicks a .psd file in Windows Explorer, Photoshop opens. When they click a mailto: link, their email app opens.

Both of those behaviors -- file association and protocol registration -- are now available to PWAs. Your web app can be the handler.

File Handling API

Two steps: declare what file types you handle in the manifest, then consume the files in JavaScript.

Step 1: Declare in the Manifest

JSON
{ "file_handlers": [ { "action": "/open", "accept": { "application/x-recipe": [".recipe"], "text/plain": [".txt"] } } ] }

action is the URL path the browser navigates to when launching the PWA with a file. accept maps MIME types to file extensions -- the same format as an <input type="file" accept=""> attribute.

When the user double-clicks a .recipe file on their desktop, the OS launches your PWA and navigates to /open. The file is not in the URL. It arrives through a queue.

Step 2: Consume the Launch Queue

JavaScript
window.launchQueue.setConsumer(async (launchParams) => { for (const handle of launchParams.files) { const file = await handle.getFile() const text = await file.text() console.log('Opened file:', file.name) console.log('Contents:', text) } })

window.launchQueue is the channel between the OS file manager and your app. launchParams.files is an array of FileSystemFileHandle objects -- the same type returned by the File System Access API. Call .getFile() to get a File object, then read it with .text(), .arrayBuffer(), or .stream().

The consumer fires once per launch. If the user opens multiple files at once, all handles arrive in a single launchParams.files array.

Availability: Chromium-based browsers on desktop, installed PWA only.

JavaScript
if ('launchQueue' in window) { // File Handling API is available }

Protocol Handlers

Protocol handlers let your PWA respond to custom URL schemes. When any link in the OS -- inside another app, in an email, in a document -- uses your protocol, your PWA opens it.

Standard Protocols via registerProtocolHandler

For standard schemes like mailto and tel, use the JavaScript API:

JavaScript
navigator.registerProtocolHandler('mailto', 'https://mymail.example/compose?to=%s')

When the user clicks a mailto:someone@example.com link anywhere on the OS, your web app opens at https://mymail.example/compose?to=mailto%3Asomeone%40example.com. The %s is replaced with the full original URL.

Custom Protocols via the Manifest

For your own protocol, declare it in the manifest. Custom protocols must have a web+ prefix -- that is a spec requirement, not a convention:

JSON
{ "protocol_handlers": [ { "protocol": "web+myrecipeapp", "url": "/handle?recipe=%s" } ] }

When someone clicks a link like web+myrecipeapp://bolognese-recipe, the OS launches your PWA and navigates to /handle?recipe=web%2Bmyrecipeapp%3A%2F%2Fbolognese-recipe.

The %s receives the full protocol URL. You extract what you need:

JavaScript
const params = new URLSearchParams(location.search) const recipeUrl = new URL(decodeURIComponent(params.get('recipe'))) const recipeName = recipeUrl.pathname.slice(1) // "bolognese-recipe"

File Handling and Protocol Handlers: OS launch flow to launchQueue, and web+ protocol routing to app URL ExpandFile Handling and Protocol Handlers: OS launch flow to launchQueue, and web+ protocol routing to app URL

PWA vs Electron

This kind of OS integration is where the question "why not just use Electron?" comes up.

Electron bundles a full Chromium engine with your app. Even a Hello World is ~50MB. The bundled engine doesn't receive browser updates -- if you don't recompile, users keep running whatever Chromium version you shipped with.

A PWA gets no bundled engine. It runs in whatever browser the user already has -- which means it automatically gets browser security updates and performance improvements. Deployments are silent: update the files on the server, users get the new version on next launch.

The tradeoff is capability. Electron can run native code, access OS APIs that browsers haven't exposed, and ship to the Mac App Store with full sandboxed access. If you need something the browser hasn't implemented yet, Electron (or its mobile equivalent, Capacitor/Cordova) is the path.

The practical rule: if the capability exists as a browser API, prefer a PWA. If it does not, you need a native wrapper.

Support

File Handling and Protocol Handlers are both Chromium-only, desktop-only, and require an installed PWA. Both are defined in the manifest -- no install, no effect.

The next OS integration is about giving content to other apps rather than receiving it. Web Share triggers the native OS share sheet from a single function call.

The Essentials

  1. File Handling: declare file_handlers in manifest with action URL and accept MIME/extension pairs. Read files via window.launchQueue.setConsumer() -- receives FileSystemFileHandle objects.
  2. Protocol Handlers: navigator.registerProtocolHandler() for standard schemes (mailto, tel). Manifest protocol_handlers for custom schemes -- custom protocols must have the web+ prefix.
  3. Both deliver data via URL replacement (%s in the URL pattern) or the launch queue -- not via URL query params directly.
  4. Both are Chromium-only, desktop-only, and require an installed PWA.
  5. PWA vs Electron tradeoff: PWA is zero-size, auto-updated, browser-sandboxed. Electron can access any OS API but ships 50MB+ and requires manual update management.

Further Reading and Watching