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



This content originally appeared on DEV Community and was authored by AYUSH SRIVASTAVA

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.


This content originally appeared on DEV Community and was authored by AYUSH SRIVASTAVA