Skip to content

Latest commit

 

History

History
108 lines (73 loc) · 4.57 KB

File metadata and controls

108 lines (73 loc) · 4.57 KB

Pitfalls, in rough order of how often they happen

1. Asking for permission on page load

You get exactly one attempt. If the user clicks "Block", that "no" is sticky and you cannot ask again from code, ever. They have to dig into site settings by hand.

The fix is a pre-prompt: your own dialog, in your own UI, explaining what the notifications are for. Only on a "yes" do you call requestPermission(). A "not now" costs you nothing; a "Block" costs you that user forever.

Implemented in demo/public/index.html and demo/public/app.js.

2. Not handling pushsubscriptionchange

The browser can rotate an endpoint on its own, with no user action. Miss this service worker event and the user stops receiving notifications while nobody finds out — your server still believes the old endpoint works. It will keep believing that until a send fails.

Most implementations in the wild simply do not have this handler. Implemented in demo/public/sw.js.

3. Deleting subscriptions on the wrong status codes

The single most damaging bug in this area. See the table in 01-concepts.md.

Short version: delete on 404/410 only. On 400/401/403 the error is yours, not the user's, and a misconfigured VAPID pair returns 401/403 on every single send — so "clean up on error" wipes your whole table on the first attempt.

4. Sending the push inside the request handler

awaiting the push send inside your HTTP handler, sequentially per device, makes the response wait on a round-trip to Google for every subscription a user has. In production this belongs in a queue (BullMQ, pg-boss, SQS). There are no retries in the naive version either: a 429 or a 5xx is lost forever.

5. Forgetting that userVisibleOnly: true is a promise

It is mandatory on Chrome and it is not decorative: you are committing to showing a visible notification for every push you receive. Use push for silent data sync and the browser will eventually revoke the permission.

6. Regenerating VAPID keys silently

If your server generates keys at runtime when it cannot find them, a lost file during a deploy mints a fresh pair — and every existing subscription dies with no error anywhere. Keys belong in environment variables, and a missing key should be a loud startup failure.

demo/generate-keys.js also refuses to overwrite an existing .env for the same reason.

7. Committing the VAPID private key

It is a secret, exactly like a JWT signing key. And it is worse than the usual leaked credential, because the correct remediation — rotating — invalidates every subscription you have. Every user has to opt in again.

Put it in .env, gitignore .env, and verify with git check-ignore -v .env.

8. Storing subscriptions per user instead of per device

One person has a phone, a tablet and a laptop. Key your storage on (user_id, endpoint), not user_id.

9. Unsubscribing in the wrong order

Call subscription.unsubscribe() in the browser first, then tell your server.

Do it the other way round and a failed unsubscribe() leaves the browser subscribed while the server has forgotten: the user gets nothing, and nothing self-heals. In the right order, a failed server call leaves an orphan row that the next send answers with 410 — which cleans itself up.

10. Putting sensitive data in the payload

It is encrypted in transit, but it is decrypted on the device and sits there on the lock screen. Send an identifier and let the client fetch the content.

11. Payload size

4096 bytes of encrypted payload. Keep your JSON under ~3KB to be safe. Over the limit you get a 413.

12. Testing iOS in a Safari tab

On iOS the site must be installed to the Home Screen as a PWA. If you test in a normal Safari tab and "it doesn't work", it is not your code.

13. Forgetting HTTPS

Required everywhere except localhost. That exemption is why the demo in this repo runs without a certificate — but the moment you test from a phone on your LAN, you need real HTTPS.

A checklist before you ship

  • VAPID keys in env vars, loud failure when missing, never committed
  • Pre-prompt before requestPermission()
  • pushsubscriptionchange handler in the service worker
  • Delete on 404/410 only
  • Honour Retry-After on 429
  • Sending happens in a queue, not in the request
  • Subscriptions keyed per device
  • TTL set to something sane for your use case
  • topic set where collapsing makes sense
  • No sensitive content in the payload
  • Tested on Chrome and Firefox
  • Tested on iOS as an installed PWA