Ship real‑time alerts without WebSocket's: Web Push for enterprise constraints 🔔

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • MyrinNew
    Senior Member
    • Feb 2024
    • 5168

    #1

    Ship real‑time alerts without WebSocket's: Web Push for enterprise constraints 🔔

    Some organizations restrict persistent connections like WebSockets, yet teams still need timely notifications even when the app isn’t open or focused.

    With Web Push—the Push API, a Service Worker, and VAPID—servers can push messages reliably without keeping a socket alive, including when the page is backgrounded or closed.


    Why Web Push

    • Works in the background via a Service Worker and shows native notifications using the Notifications API for consistent, system‑level UX.
    • Standards‑based, requires HTTPS, and uses VAPID keys so your server is identified securely to push services.


    How it fits together

    • App registers a Service Worker and requests notification permission from the user on a secure origin.
    • App subscribes with Push Manager to get a unique subscription endpoint and keys for that browser/device.
    • Server stores subscriptions and later sends payloads signed with VAPID using a lightweight library.
    • The Service Worker receives the push event and displays a native notification immediately.


    Client: register SW and subscribe





    // Convert base64 VAPID public key to Uint8Array
    function base64ToUint8Array(base64) {
    const padding = '='.repeat((4 - (base64.length % 4)) % 4);
    const b64 = (base64 + padding).replace(/-/g, '+').replace(/_/g, '/');
    const raw = atob(b64);
    const output = new Uint8Array(raw.length);
    for (let i = 0; i raw.length; ++i) output[i] = raw.charCodeAt(i);
    return output;
    }

    async function subscribeToPush(vapidPublicKeyBase64) {
    const registration = await navigator.serviceWorker.register('/sw.js');
    const permission = await Notification.requestPermission();
    if (permission !== 'granted') return;

    const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: base64ToUint8Array(vapidPublicKeyBase64),
    });

    await fetch('/api/push/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(subscription),
    });
    }








    The app subscribes via the Push API on a secure context and sends the resulting subscription to the backend for later use.


    Service Worker: receive and notify






    // /sw.js
    self.addEventListener('push', (event) => {
    const data = event.data ? event.data.json() : { title: 'Update', body: 'New alert' };
    event.waitUntil(
    self.registration.showNotification(data.title, {
    body: data.body,
    icon: '/icon.png',
    data: data.url || '/',
    })
    );
    });

    self.addEventListener('notificationclick', (event) => {
    event.notification.close();
    const url = event.notification.data || '/';
    event.waitUntil(clients.openWindow(url));
    });








    The Service Worker handles the push event payload and displays a native notification using the Notifications API.


    Server (Node/Express): VAPID and send






    // npm i express web-push
    import express from 'express';
    import webpush from 'web-push';

    const app = express();
    app.use(express.json());

    // 1) Configure VAPID (generate once and set via env)
    webpush.setVapidDetails(
    'mailto:admin@example.com',
    process.env.VAPID_PUBLIC_KEY,
    process.env.VAPID_PRIVATE_KEY
    );

    // 2) Store subscriptions (replace with MongoDB in production)
    const subscriptions = new Map();

    app.get('/api/push/public-key', (_req, res) => {
    res.json({ publicKey: process.env.VAPID_PUBLIC_KEY });
    });

    app.post('/api/push/subscribe', (req, res) => {
    const sub = req.body;
    subscriptions.set(sub.endpoint, sub);
    res.status(201).json({ ok: true });
    });

    app.post('/api/push/send', async (req, res) => {
    const payload = JSON.stringify({
    title: 'Policy update',
    body: 'Click to review changes',
    url: '/inbox',
    });
    const results = [];
    for (const sub of subscriptions.values()) {
    try {
    await webpush.sendNotification(sub, payload);
    results.push({ ok: true });
    } catch {
    results.push({ ok: false });
    }
    }
    res.json({ sent: results.length });
    });

    app.listen(3000, () => console.log('Server running on 3000'));








    The web‑push library signs payloads with VAPID and delivers to each saved subscription endpoint, letting servers send messages without maintaining a persistent connection.


    Practical tips

    • Only request permission at meaningful moments to avoid prompt fatigue and improve opt‑in rates.
    • Subscriptions can expire; handle send failures by pruning invalid endpoints and re‑subscribing when needed.
    • Push requires HTTPS and secure contexts; keep VAPID keys safe and reuse the same key pair across deploys per environment policy


    If WebSocket's are off the table, Web Push gives reliable, secure, background delivery with a small footprint—perfect for “must‑know” alerts in constrained environments.




    More...
Working...