Skip to content

Instantly share code, notes, and snippets.

@MirzaLeka
Last active April 27, 2025 13:05
Show Gist options
  • Save MirzaLeka/335d6f8a01ac88909f61d11ab688bac0 to your computer and use it in GitHub Desktop.
Save MirzaLeka/335d6f8a01ac88909f61d11ab688bac0 to your computer and use it in GitHub Desktop.
HTML & JS Push Notifications

Project Info

This example demonstrates how to handle browser push notifications using vanilla HTML & JavaScript via Web Push API. WebPush API sends messages from the server (backend) to the client (browser/mobile phone). The API uses HTTP2 protocol which requires HTTPS connection (unless you're in localhost) The server sends messages to the client's browser via Firebase Cloud Messaging API (or FCM for short).

Prerequisites

  • You need to obtain the public VAPID key to be able to connect to the server. Look up generate-VAPID-key.md file.
  • Make sure Backend server uses the same VAPID keys.

Folder structure

  • The index.html sets up the application template and connects to the scripts.js file
  • The scripts file registers a browser service-worker (via service-worker.js file in the root directory). This file also calls specified API endpoints in response to button clicks on UI
  • The service-worker.js files handles notifications.
service-worker.js
📁 public 
|__ index.html
|__ scripts.js

Run the project

// PushController.cs
using Microsoft.AspNetCore.Mvc;
using System.Collections.Concurrent;
using WebPush;
namespace WebPushAPI.Controllers
{
public class PushSubscriptionRequest
{
public string Endpoint { get; set; }
public Dictionary<string, string> Keys { get; set; }
}
[Route("api/[controller]")]
[ApiController]
public class PushController : ControllerBase
{
private static readonly ConcurrentBag<PushSubscription> _subscriptions = new();
private readonly WebPushClient _client;
private readonly VapidDetails _vapidDetails;
public PushController(WebPushClient client)
{
_client = client;
_vapidDetails = new VapidDetails(
"mailto:[email protected]", // any random mail will do
"PUBLIC_KEY",
"PRIVATE_KEY"
);
}
[HttpPost("subscribe")]
public IActionResult Subscribe([FromBody] PushSubscriptionRequest requestBody)
{
requestBody.Keys.TryGetValue("p256dh", out string? p256dh);
requestBody.Keys.TryGetValue("auth", out string? auth);
var subscription = new PushSubscription
{
Endpoint = requestBody.Endpoint,
P256DH = p256dh,
Auth = auth
};
var user = Request.Headers["X-User"].ToString();
Console.WriteLine($"User: {user}");
Console.WriteLine($"Subscription: {subscription.Endpoint}");
_subscriptions.Add(subscription);
return Created(string.Empty, new { message = "Subscribed successfully" });
}
[HttpPost("send-notification")]
public async Task<IActionResult> SendNotification()
{
await Task.Delay(5000); // simulate delay
var payload = new
{
title = "Hello!",
body = "This is a push notification.",
data = new { url = "https://www.app.com", id = "123" }
};
var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
foreach (var sub in _subscriptions)
{
try
{
await _client.SendNotificationAsync(sub, jsonPayload, _vapidDetails);
}
catch (WebPushException ex)
{
Console.WriteLine($"Push error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Push error: {ex.Message}");
}
}
return Ok(new { message = "Notifications sent" });
}
}
}

Generate VAPID Key

WebPush API requires a VAPID authentication control. VAPID auth consists of two keys:

  • Public key
  • Private key

The Public key is stored in the browser and on the server. The Private key is stored only on the server and should not be shared.

  • To geneate your keys set up a quick Node.js project:
npm init -y
  • Then install the web-push library from NPM.
npm i web-push
  • Create app.js file and paste the following
const webpush = require('web-push');

// VAPID keys should be generated only once.
const vapidKeys = webpush.generateVAPIDKeys();

console.log('vapidKeys :>> ', vapidKeys);
  • Run app.js file (node app.js)
// VAPID keys example
vapidKeys :>>  {
  publicKey: 'BDc...',
  privateKey: 'xHt....'
}

When using the VAPID keys in the Push Manager,

  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
  });

it creates an object that looks like:

const subscriptionPayload = {
  endpoint: 'https://fcm.googleapi.com/fcm/send/...', // this will be automatically generated via VAPID keys
  keys: {
    auth: '.....', // this will be automatically generated via VAPID keys
    p256dh: '.....' // this will be automatically generated via VAPID keys
  }
}

This is object payload then sent to the server that authenticates the request using VAPID Public and Private keys. The server then uses the Private key to generate a push notification.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Web Push Demo</title>
</head>
<body>
<h2>Push Notification Demo</h2>
<button id="subscribe">Subscribe</button>
<button id="notify">Send Notification</button>
<script src="./scripts.js"></script>
</body>
</html>
const PUBLIC_VAPID_KEY = 'YOUR_PUBLIC_KEY'; // Replace this!
// backend API
const subscribeURL = 'https://localhost:7254/api/Push/subscribe';
const notifyURL = 'https://localhost:7254/api/Push/send-notification';
// register service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(() => console.log('Service Worker registered'));
}
// handle subscribe button click
document.getElementById('subscribe').onclick = async () => {
// parse VAPID keys into subscription payload
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
});
// call subscribe API
await fetch(subscribeURL, {
method: 'POST',
body: JSON.stringify(subscription),
headers: { 'Content-Type': 'application/json', 'X-User': 'Mirzly94' }
});
alert('Subscribed!');
};
// handle notify button click
document.getElementById('notify').onclick = async () => {
// call send-notification API
await fetch(notifyURL, { method: 'POST' });
};
// parse VAPID key (request body)
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
self.addEventListener('push', event => {
const data = event.data.json();
console.log('Push received:', data);
self.registration.showNotification(data.title, {
body: data.body
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment