Build a push-enabled server with Express and web-push
This codelab walks through creating a push notification server with Express.js. When you finish, the server will persist push subscription records, deliver a notification to a single subscriber, and broadcast to every active subscription.
The client-side code is supplied and complete; you only implement the server. If you need background on the client side or the underlying protocol, see the push notification overview and the companion client codelab.
Environment notes
This project is verified on Windows with Chrome or Edge, and on macOS and Android with Chrome or Firefox. It does not work on iOS, nor on macOS with Brave, Edge, or Safari.
The server uses Express.js for routing, the web-push Node library for all push logic, and lowdb to persist subscriptions to a JSON file. These are choices for reliability in this exercise; push notifications don't mandate any particular stack.
Key setup
Both server and client need authentication keys before notifications will work. Generate a VAPID key pair from the terminal:
- Run
npx web-push generate-vapid-keysand copy the output values. - Edit
.envand paste the keys intoVAPID_PUBLIC_KEYandVAPID_PRIVATE_KEY. SetVAPID_SUBJECTtomailto:[email protected]. All values belong inside double quotes.
VAPID_PUBLIC_KEY="BKiwTvD9HA…"
VAPID_PRIVATE_KEY="4mXG9jBUaU…"
VAPID_SUBJECT="mailto:[email protected]"
- Open
public/index.jsand swapVAPID_PUBLIC_KEY_VALUE_HEREfor the public key value.
Your browser asks for permission when the client first subscribes. In the companion app tab, use Register service worker and then Subscribe to push; choose Allow when prompted. The status output should confirm the registration and subscription steps.
Open the terminal logs after subscribing. A /add-subscription POST arrives at the server carrying the new subscription record. That payload is what the server needs to store.
Subscription persistence
The subscription lifecycle is mostly handled by the client; the server only stores new records and removes canceled ones. Those records are what let the server address messages later.
Storing a new subscription
Implement the /add-subscription route with the code below:
app.post('/add-subscription', (request, response) => {
console.log('/add-subscription');
console.log(request.body);
console.log(`Subscribing ${request.body.endpoint}`);
db.get('subscriptions')
.push(request.body)
.write();
response.sendStatus(200);
});
Removing a canceled subscription
Back in the app, select Unsubscribe from push. The terminal shows a /remove-subscription POST with the client's subscription data.
For the corresponding server route, use this logic:
app.post('/remove-subscription', (request, response) => {
console.log('/remove-subscription');
console.log(request.body);
console.log(`Unsubscribing ${request.body.endpoint}`);
db.get('subscriptions')
.remove({endpoint: request.body.endpoint})
.write();
response.sendStatus(200);
});
Triggering notifications
A push message doesn't travel from your server straight to the browser. The server makes a web push protocol request to the push service run by the user's browser vendor, and that service does the actual delivery. Your code below initiates that request for an individual subscriber.
Update the /notify-me route:
app.post('/notify-me', (request, response) => {
console.log('/notify-me');
console.log(request.body);
console.log(`Notifying ${request.body.endpoint}`);
const subscription =
db.get('subscriptions').find({endpoint: request.body.endpoint}).value();
sendNotifications([subscription]);
response.sendStatus(200);
});
Then give sendNotifications() its implementation, which extracts subscription records and asks the push service to deliver the payload:
function sendNotifications(subscriptions) {
// Create the notification content.
const notification = JSON.stringify({
title: "Hello, Notifications!",
options: {
body: `ID: ${Math.floor(Math.random() * 100)}`
}
});
// Customize how the push service should attempt to deliver the push message.
// And provide authentication information.
const options = {
TTL: 10000,
vapidDetails: vapidDetails
};
// Send a push message to each client specified in the subscriptions array.
subscriptions.forEach(subscription => {
const endpoint = subscription.endpoint;
const id = endpoint.substr((endpoint.length - 8), endpoint.length);
webpush.sendNotification(subscription, notification, options)
.then(result => {
console.log(`Endpoint ID: ${id}`);
console.log(`Result: ${result.statusCode}`);
})
.catch(error => {
console.log(`Endpoint ID: ${id}`);
console.log(`Error: ${error} `);
});
});
}
For a broadcast to all subscribers, wire up the /notify-all route:
app.post('/notify-all', (request, response) => {
console.log('/notify-all');
response.sendStatus(200);
console.log('Notifying all subscribers');
const subscriptions =
db.get('subscriptions').cloneDeep().value();
if (subscriptions.length > 0) {
sendNotifications(subscriptions);
response.sendStatus(200);
} else {
response.sendStatus(409);
}
});
Return to the app tab and press Notify me. The resulting notification has the title Hello, Notifications! and the body ID: <ID>, where the ID is a random number.
To verify the /notify-all path, subscribe from additional browsers or devices on supported platforms, then click Notify all. Each active subscription receives the identical notification.
Further reading
- The push notifications overview explains the full protocol and the role of the push service.
- The push notification client codelab covers requesting permission, managing subscriptions from the page, and displaying incoming messages via a service worker.



