70 lines
4.4 KiB
Markdown
70 lines
4.4 KiB
Markdown
# Design note: PWA with Web Push
|
|
|
|
## Decision
|
|
|
|
The companion is an installable web app (PWA) that gets notifications through Web Push. It is plain ES modules with
|
|
no build step, served by Sandbar itself at `/app/` or from any static host.
|
|
|
|
## What it has to do
|
|
|
|
1. Sign in to a Sandbar server.
|
|
2. Follow topics and get push notifications while the app is closed.
|
|
3. Acknowledge, snooze, escalate and resolve on-call alerts, ideally from the notification.
|
|
4. Show monitors and Hammerhead tunnel status, read-only.
|
|
|
|
## Options
|
|
|
|
| | PWA + Web Push | Flutter or React Native | Native (Kotlin, Swift) |
|
|
| --- | --- | --- | --- |
|
|
| Android push | Yes, through the browser's push service | FCM, needs a Firebase project | FCM, same |
|
|
| iPhone push | Yes since iOS 16.4, once added to the Home Screen | APNs, needs a paid developer account | Same |
|
|
| Server needs | Standard Web Push (VAPID), no vendor account | A Firebase or APNs sender, or a relay | Same |
|
|
| Self-hosting fit | Good: nothing leaves your server except the encrypted push | Poor: FCM project keys, or a third-party relay | Poor |
|
|
| Distribution | A URL, "Add to Home Screen" | Play Store, App Store, signing, review | Same |
|
|
| Notification buttons | Yes (Acknowledge, Snooze) on Android and desktop; limited on iOS | Yes | Yes |
|
|
| Background connection | Push only, no always-on socket | Same | Same |
|
|
| Maintenance | One small code base, one language | A framework and two store pipelines | Two code bases |
|
|
|
|
## Why a PWA
|
|
|
|
- **Sandbar is self-hosted and one binary.** Native push needs either a Firebase project baked into a store app or a relay
|
|
that sees every notification. Web Push needs neither: each browser vendor runs the push service, Sandbar signs with its own
|
|
VAPID key, and the payload is encrypted end to end (RFC 8291), so the push service sees only ciphertext.
|
|
- **Nothing to publish.** An admin points people at `https://sandbar.example.com/app/`. A store app would have to be a
|
|
public, multi-tenant client with a server address field, built and signed by someone.
|
|
- **The sharks stack is web already.** The Sandbar and Hammerhead UIs use the same tokens, so this reuses them.
|
|
- **The work is mostly API calls.** Sandbar already has everything except push, so a thin client is enough.
|
|
|
|
## What we give up
|
|
|
|
- **iPhone** only gets push after Add to Home Screen, and notification action buttons are not shown there. Tapping the
|
|
notification opens the app, where the buttons are one tap away.
|
|
- **Delivery is best effort**, through Google, Apple or Mozilla. For a pager that is not enough on its own. Sandbar's
|
|
escalation policies still email and repeat, and the existing ntfy-app path keeps working for people who want the
|
|
ntfy app's persistent connection and Do Not Disturb override. The PWA is a companion, not a replacement.
|
|
- **No critical-alert bypass** of Do Not Disturb. A native app could ask for that entitlement.
|
|
- **Background fetch is limited**, so when the app is closed it only knows what pushes told it. It refreshes on open.
|
|
|
|
If those limits ever outweigh the benefit of skipping the stores, the same API (`docs/mobile-api.md` in Sandbar) serves a
|
|
native client unchanged: the server side is one table of subscriptions and one push sender. A Flutter app would add FCM
|
|
as a second sender.
|
|
|
|
## How it fits together
|
|
|
|
```
|
|
phone (PWA) --login--> Sandbar /api/v1/mobile/login -> API key (bearer token)
|
|
phone --subscribe-> Sandbar /api/v1/push/subscriptions {endpoint, keys, topics}
|
|
Sandbar Publish() --> push service (Google/Apple/Mozilla) --> service worker --> notification
|
|
notification button --> service worker --> POST /api/v1/alerts/{id}/ack (bearer token from IndexedDB)
|
|
```
|
|
|
|
- **Sign-in** returns an API key named after the device, not a cookie. It works from any origin, and signing out of one
|
|
phone deletes only that key.
|
|
- **Service worker** is a classic script (Firefox does not run module workers). It cannot read `localStorage`, so the sign-in
|
|
is kept in IndexedDB.
|
|
- **Open app**: while visible it streams each followed topic over `/{topic}/json` and suppresses the system notification.
|
|
- **Subscription rotation** (`pushsubscriptionchange`) re-registers itself, and a 404 or 410 from the push service
|
|
removes the server-side row.
|
|
- **Hammerhead** status goes through Sandbar (`/api/v1/hammerhead/edges`) because Hammerhead lists edges only for
|
|
signed-in people, not API keys. Sandbar holds that sign-in so the phone never gets Hammerhead credentials, and edge tokens are stripped.
|