Building the server side of a push notifications stack
The client-facing work of a push notifications app often gets the attention, but the server has a critical job: keeping track of subscriptions and reliably getting messages to them. This walkthrough covers the server-side pieces you need to write yourself, starting from a working Express app with the client code already finished.
The sample app you start with already has a UI for registering a service worker, subscribing to push, and asking the server to notify either the current subscription or every subscription in its database. The client code is complete; the server file server.js contains four marked TODO: items you'll implement: loading VAPID details, sending notifications, handling new subscriptions, and handling subscription cancellations.
As you click through the UI in a separate Chrome tab, keep the Glitch logs open (Tools → Logs). That's where you'll see server-side output like Listening on port 3000, as well as messages from any notify buttons you've already tried. Notifications are blocked inside the embedded Glitch preview, so view the live app in its own tab while you edit code back in Glitch.
Generate and load VAPID keys
Why VAPID matters
VAPID (Voluntary Application Server Identification for Web Push) is the mechanism that lets a browser trust the identity of the server behind a push subscription. It uses public-key cryptography so that an app can prove who it is to subscription endpoints and encrypt notification content, and users can be confident a notification really comes from the app that set up the subscription.
Getting the keys into the project
This app relies on the web-push npm package, which can generate VAPID keys and handle encryption and delivery of messages.
Start by uncommenting the key generation lines in server.js that call the generateVAPIDKeys function. When Glitch restarts the app, those keys are printed to the Glitch logs, not the Chrome console. Copy the public and private keys as a pair—the output will scroll quickly because the app restarts on every code change.
Next, put both keys into the .env file in double quotes, and set VAPID_SUBJECT to something like "mailto:[email protected]". Comment the generation lines back out once you've captured the keys, since this only needs to happen once. In that same .env, load the values back into the server by reading the environment variables.
Finally, copy the public key into the client constant VAPID_PUBLIC_KEY in public/index.js. That constant is what the browser uses when it creates a subscription, so it must match the key the server will use later.
Sending notifications from the server
The web-push package makes delivery straightforward: calling webpush.sendNotification() encrypts the payload automatically. It also accepts options for finer control, but this app only needs two of them:
const options = {
TTL: 86400,
vapidDetails: {
subject: process.env.VAPID_SUBJECT,
publicKey: process.env.VAPID_PUBLIC_KEY,
privateKey: process.env.VAPID_PRIVATE_KEY
}
};
The TTL (time-to-live) value puts an expiry on the message, which lets the push service avoid delivering a stale notification to a user. The vapidDetails object carries the identity of the server to the push endpoint.
In server.js, the sendNotifications function needs updating to iterate over the subscriptions it's given and call webpush.sendNotification() for each endpoint. Because that function returns a promise, error handling fits naturally: check the statusCode of any rejection and log whether the failure is due to a missing subscription (404 or 410) or some other problem.
Registering a new subscription
When a user subscribes in the browser, the flow looks like this:
- The user clicks Subscribe to push.
- The client uses the server's public VAPID key to generate a subscription object—a unique, server-specific endpoint along with encryption keys.
- The client sends that subscription as stringified JSON in a
POSTrequest to/add-subscription. - The server parses the body and stores the subscription in its database, keyed by the subscription's own endpoint.
The route handler stub in server.js needs three steps: pull the subscription out of the request body, parse it back to an object, and add it to the active subscriptions store. Once that's in place, the new endpoint becomes eligible to receive notifications.
Cleaning up cancelled subscriptions
The server won't always know when a subscription goes stale—browsers can discard them when a service worker is shut down. But the app UI does surface cancellations, and handling them keeps the server from pushing messages to dead endpoints. That matters more at scale than in a test app, but it's worth doing properly.
Cancellation requests arrive at the /remove-subscription POST route. The body of that request contains just the endpoint string the client wants to remove. The route handler needs to read that endpoint from the body, look it up in the subscriptions database, and delete it.
With those four server pieces in place—VAPID setup, notification sending, subscription addition, and subscription removal—the server becomes a functional hub for managing and delivering push messages.



