About

Turning a Local Dev Page into a Mac App — One Manifest and launchd, No Electron

Not an iframe, not a wrapper app. The browser gives the page its own window

There's a personal page I open every morning. It runs on a local Rails dev server. I wanted it to behave like a Mac app instead of a browser tab. Electron and Tauri came to mind first, but it ended with one web app manifest and one launchd job.

Follow along and a dev page like http://myapp.localhost:3000/ becomes its own window with a Dock icon.

What turns into an app

It's not an iframe, a box inside another page. The browser gives the page its own window: no tabs, no address bar, its own Cmd+Tab entry. The window is the page.

Dock icon My Desk no tabs · no URL bar · own Cmd+Tab entry Chrome app window (from manifest.json) Rails dev server myapp.localhost:3000 same code, no cache launchd at login start only if the port is free

Three files and one line

Put the manifest and two icons in public/, then add one line to the layout's <head>.

{
  "name": "My Desk",
  "short_name": "Desk",
  "start_url": "/",
  "display": "standalone",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}

<link rel="manifest" href="/manifest.json">

I got stuck twice.

I first named the file manifest.webmanifest. The Rails dev server served it as text/plain. Renamed to manifest.json, it came back as application/json.

I also assumed a service worker was required for the install button. It wasn't. On 2026-10-01, macOS 14.6.1 with Chrome 154, the localhost page received beforeinstallprompt with no service worker at all. Installing created an app under ~/Applications/Chrome Apps/. No service worker means no cache, so code changes show up in the window as usual. Only manifest changes such as the name or icon reach Chrome late.

If you add your own install button

prompt() on beforeinstallprompt works once. Open it, dismiss it, and the button stays dead until a reload. Once the chance is used, I show a hint instead: the install icon at the right of the address bar, or the ⋮ menu.

let deferred = null;
window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  deferred = e;
  installButton.hidden = false;
});
installButton.addEventListener('click', async () => {
  if (!deferred) { showMenuHint(); return; }
  deferred.prompt();
  await deferred.userChoice;
  deferred = null; // can't be used twice
});

The install confirmation appears outside the page, in the browser's own UI. Automation such as an extension can't press that final "Install". A person does.

launchd starts the server

A Dock icon is useless if the server is down, so launchd starts the dev server at login.

Our repo's start command (make lad) first runs kill -9 on whatever holds the port. So the job starts it only when the port is free. AbandonProcessGroup keeps the server alive when the launchd job itself exits.

<key>ProgramArguments</key>
<array>
  <string>/bin/zsh</string><string>-lc</string>
  <string>lsof -iTCP:3000 -sTCP:LISTEN || (cd ~/src/myapp &amp;&amp; make lad)</string>
</array>
<key>RunAtLoad</key><true/>
<key>AbandonProcessGroup</key><true/>

Next to Electron and Tauri

Approach What it takes What you get
install via manifest 3 files + one link tag Dock icon, window without address bar, Cmd+Tab
Tauri or Electron wrapper new project, builds, signing menu bar, global shortcuts, remembered window state
standalone app bundling the server ship Rails and the DB offline use

A wrapper still shows the same page inside. If you truly need a menu bar icon or global shortcuts, go Tauri. For a personal tool that just has to open from the Dock, the manifest is enough. Another build and signing pipeline is only more upkeep.

Not checked yet: whether Safari's "Add to Dock" picks up the same manifest, and what the window shows when the server is down.

If you want an internal admin page or a personal tool to feel like an app and aren't sure how far to build, ask us at https://www.negabaro.com/contact.

How It Works

1

Put manifest.json and 192/512 icons in public/, add one link tag to <head>

2

Install the page as an app in Chrome: Dock icon and its own window

3

launchd starts the dev server at login, only if the port is free

Pros

  • ✓ No build or signing; code changes show up as usual
  • ✓ Installed without a service worker

Cons

  • ✗ No menu bar icon or global shortcuts
  • ✗ A person has to press the final install button
← Our Postgres Volume Path Was Wrong for 3 Years and Nothing Broke — The Anonymous Volume Behind VOLUME Finder Color Tags Weren't AI — Just a "Red\n6" String in an xattr →