Skip to content

Latest commit

 

History

History
121 lines (85 loc) · 4.49 KB

File metadata and controls

121 lines (85 loc) · 4.49 KB

The demo

The full lifecycle of a push notification in ~850 lines without a framework or a build step: permission → subscription → store → send → notification.

Running it

npm install
node generate-keys.js    # once: writes the VAPID pair to .env
npm start                # http://localhost:3456

Files

File What it holds
generate-keys.js One-shot VAPID key generation. Refuses to overwrite an existing .env.
server.js Four routes, the send logic, and the status-code handling.
public/index.html Three buttons and an on-page log, plus the pre-prompt dialog.
public/app.js Permission, subscribe, unsubscribe. Everything here is frontend → backend.
public/sw.js The service worker: push, notificationclick, pushsubscriptionchange.
subscriptions.json The "database". A flat file on purpose, so you can read it.

Routes

Route What it does
GET /api/vapid-public-key The public key. The client needs it for subscribe().
POST /api/subscriptions Stores {endpoint, keys}. Idempotent per endpoint.
DELETE /api/subscriptions Removes by endpoint.
POST /api/send Pushes to everyone. Accepts {"delay": 5}.

Six experiments worth running

1. Find out who your push service is

Open the page in Chrome and enable notifications. The server console prints:

Push service for THIS browser: fcm.googleapis.com

That host is where your backend POSTs. It is Google, and this project has no Google credentials at all (grep -ri firebase *.js public/*.js returns nothing).

2. Read it with your own eyes

cat subscriptions.json

The endpoint is right there in plain text. That is why this demo's "database" is a JSON file and not SQLite.

3. Prove that push is not a WebSocket

Hit Send push (5s) and close the entire browser before the five seconds elapse.

The notification arrives anyway. A WebSocket dies with the tab; the service worker does not.

4. Prove the same code covers every browser

Repeat everything in Firefox without changing a line. The console now says:

Push service for THIS browser: updates.push.services.mozilla.com

Zero per-browser branching. That is what the standard gives you.

5. Watch the error handling do the right thing

Delete one character from VAPID_PRIVATE_KEY in .env, restart, and send a push:

[403] YOUR OWN error against fcm.googleapis.com: permission denied: invalid JWT provided
      NOT deleting the subscription. Check your VAPID keys or payload.

The subscription is still there. That is the point — plenty of real code deletes it here, which means one misconfigured key wipes the entire table on the first send.

Code Meaning What this demo does
404 / 410 The subscription is genuinely dead Deletes. The only correct case.
400 Malformed request Logs. Keeps.
401 / 403 VAPID problem Logs. Keeps.
413 Payload > 4096 bytes Logs. Keeps.
429 Rate limited Honours Retry-After. Keeps.

6. Confirm the server really talks to Google — no browser required

node -e "
const c=require('crypto'), e=c.createECDH('prime256v1'); e.generateKeys();
console.log(JSON.stringify({endpoint:'https://fcm.googleapis.com/fcm/send/FAKE-TOKEN',
  keys:{p256dh:e.getPublicKey().toString('base64url'),auth:c.randomBytes(16).toString('base64url')}}));
" | curl -s -X POST localhost:3456/api/subscriptions -H 'Content-Type: application/json' -d @-

curl -s -X POST localhost:3456/api/send -H 'Content-Type: application/json' -d '{"delay":0}'

The log shows [410] dead subscription at fcm.googleapis.com -> deleted (correct). That 410 came back from Google, in a real HTTP response, because the token was made up.

Two things to know before production

Rotating VAPID keys invalidates EVERY existing subscription. Each one was minted by the browser against your old public key and only accepts pushes signed with the matching private key. That is why generate-keys.js refuses to overwrite an existing .env — and why a leaked private key is expensive to fix.

The permission has one attempt. If the user clicks "Block", that "no" is sticky and cannot be requested again from code. Hence the pre-prompt.

On a phone

  • You need real HTTPS (localhost does not help from another device).
  • On iOS the site must be installed to the Home Screen as a PWA. From a normal Safari tab there is no push, and that is not a bug in your code.