Pier

Your site is the desktop app

Pier is a small runtime on the person’s Mac. It reads the web app manifest you already ship and opens your site in a window of its own, with the files, ports and devices a browser keeps out of reach.

0.5.0, macOS 14 and later, 3.2 MB

You can already install a PWA

Chrome and Safari put your site in a window without tabs, and for many sites that is enough. It is still the browser, so what your page can reach depends on which one the person opened. This is what changes when Pier opens the same site instead.

Installed from the browser Opened by Pier
Hardware Chromium only, and it refuses whole classes of device outright; Safari and Firefox have none of it Real serial ports, USB endpoints, HID reports, Bluetooth LE and MIDI — what a native app gets, minus input devices and security keys
Network Fetch under CORS, WebSocket Fetch without CORS, TCP, TLS, UDP, Bonjour
Files A sandbox in Chrome, uploads in Safari System panels, and what was picked stays readable
Closed window The page stops Audio, sockets and notifications keep going
Secrets localStorage Keychain, behind Touch ID
Menus The browser’s Your own menu bar, Dock menu and global shortcuts

The trade is the install: a PWA needs the browser people already have, a Pier app needs Pier first.

Nothing to build, nothing to ship

No bundle

No Electron, no Chromium in your download, no build pipeline for a second target. Pier is 3.2 MB and already on the person’s machine; the app it makes is a shortcut to your site with native powers.

No certificate, no store

No developer account, no notarization, no review, no yearly fee. The app is built and signed on the person’s own machine, so it opens without warnings.

No second release train

Ship as usual. The app loads your site, so a deploy reaches everyone at once — no version to bump, no update to push, nobody stuck on an old build.

What your users get

A window, not a tab

Tabs, the address bar and someone else’s buttons go away, and the page runs the same code it always did. It gets a real titlebar, a menu bar of its own and keyboard shortcuts that work even when the window is hidden.

A link outside your scope opens in the browser, so the app stays your app.

An app that keeps running

Closing the window does not kill it: audio keeps playing, connections stay up, notifications still arrive. Sign-ins survive quitting, and the first launch opens on the page the person was reading in the tab.

The Pier window listing two installed apps
Apps land in ~/Applications/Pier, so Spotlight and Launchpad find them like any other. Removing one is dragging it to the Trash.

What your code gets

window.epwa appears only inside the app, so the same code still runs in any browser. Declare what you need in the manifest; anything you did not declare answers with an error, even when your code asks for it.

DeclareAnd your page can
nothing Drive the window and titlebar, fill the menu bar and the Dock menu, register global shortcuts, keep running in the background, store secrets in the Keychain, ask for Touch ID
files Open and save through system panels, then read, write, watch and trash what the person picked — across launches, without ever seeing a path
network Fetch without CORS, open TCP, TLS and UDP sockets, browse Bonjour, accept incoming connections (loopback unless the person allows more)
serial usb hid bluetooth midi Talk to the device the person picked in the system chooser. Keyboards, pointers and security keys are never handed over
notifications Post native notifications with buttons, a reply field and attachments, and hear about every click
camera microphone location screen Ask the operating system. It prompts, it decides, and Pier cannot work around the answer

144 methods and 50 events in total: the full reference is generated from the same spec the runtime is tested against, so it cannot drift from the code.

What you write

Two things, once: a few keys in the manifest you probably already have, and a button that opens an epwa:// link. That is the whole integration.

manifest.webmanifest
{
  "name": "Phonotheque",
  "id": "/app",
  "start_url": "/app",
  "scope": "/app",
  "display": "standalone",
  "icons": [{ "src": "/icon-512.png", "sizes": "512x512" }],
  "epwa": { "permissions": ["files", "midi"] }
}
one button on your page
import { canInstallApp, launchApp } from './pier-launch.js';

button.hidden = !canInstallApp();
button.onclick = () => launchApp({
  manifest: '/manifest.webmanifest',
  handoff: { track: player.currentTrack },
  onLaunched: () => player.pause(),
  onMissing: () => downloadPrompt.show(),
});

The helper is one file, no build step; it hides the button where Pier cannot run at all, hands the app whatever state the visitor had in the tab, and tells you when a click went nowhere so you can offer the download. Lint a manifest before you publish it: web-runtime check <url>.

The honest part

Install it and see

Point it at a site you are working on and watch the tab become an app. Pier updates itself, so this is the only download.