Prufy notes
The interface ยท 6 min

Installable, with push

A manifest, generated icons and a tiny service worker make Prufy a web app you can install. The reason is push: on an iPhone, an installed app is the only place it works.

What makes it installable

app/manifest.ts
The name, a black background and theme colour, display: "standalone" so it opens without the browser's address bar, and three icons.
app/icons/[size]/route.tsx
One route draws the icon at five sizes from MARK_PATH, imported from components/logo.tsx, so there are no PNG files to keep in sync.
public/sw.js
The service worker. It shows a notification when one is pushed and opens the right claim when it's tapped. That's all it does.
components/service-worker.tsx
Registers the worker on every page load, not when alerts are turned on, so it's already active by the time it's needed.
IconSizeUsed by
/icons/192192pxThe manifest, and the notification's icon
/icons/512512pxThe manifest
/icons/apple180pxThe iPhone Home Screen, from the layout's metadata
/icons/maskable512px, more paddingAndroid, which crops it to a circle or squircle
/icons/badge96px, no tileThe small mark Android shows in the status bar

The icon route is marked force-static, with its five names listed in generateStaticParams, and its response is cacheable for a year. Any other name is a 404.

A service worker that caches nothing

A is usually how a web app works offline: it answers requests from a cache. This one deliberately doesn't.

The whole service worker: four listenersStep through them
Step 1 of 4
install
self.addEventListener("install", () => {
  self.skipWaiting()
})

A new version of the file takes over as soon as it's installed, without waiting for old tabs to close. A fix to the worker reaches people quickly.

apps/web/public/sw.js. There's no fetch listener, which is what caching would need.
If it cached pages
Opening the app could show yesterday's answer. A stale "verified" on a domain that went at risk overnight is worse than a spinner.
Caching nothing
Every page asks the server and shows live status. The cost is that there's no offline mode at all.
Predict it first
You open the installed app on a plane, with no connection. What do you see?

Subscribing, and a push arriving

From a switch to a notificationStep through it
You
Browser
Prufy
Push service
Step 1 of 7
It has to start with a click
Browsers ignore a permission prompt that the person didn't ask for, so subscribeToPush is only ever called from the switch.
apps/web/components/claim/alerts-card.tsx

is why no vendor account is needed. The public key is safe to publish, and the browser gets it from capabilities.pushPublicKey. Make a pair with bunx web-push generate-vapid-keys.

What a browser can and can't do

What the alerts card offers, by browserPick a situation
When
An iPhone or iPad in a browser tab: the push APIs aren't there, and the site isn't installed.
The card shows
No switch. 'On iPhone, notifications only work from the Home Screen. Tap Share, then Add to Home Screen, and start there. Email works everywhere.'
pushSupport() in apps/web/lib/push-client.ts returns one of these four. The words are PUSH_NOTES in components/claim/alerts-card.tsx.

The iPhone limitation

Prufy in a Safari tab
Has the session cookie from your first claim, so it shows your domains. Can't receive push.
Prufy on the Home Screen
Can receive push. But it's a separate app with its own cookies, so it has no session and none of the domains you started in Safari.

iOS only allows web push from a site that's been added to the Home Screen. The two halves above don't share cookies, and Prufy has no way to sign in on a second device yet. So on an iPhone today, push only covers domains claimed from inside the installed app. Email has no such limit, which is why the alerts card lists it first and the iPhone note ends with "Email works everywhere".

The reasoning is in docs/decisions/0010-the-landing-page-is-the-app.md, under Installable, and in 0009 for the iPhone consequence.