Skip to content
Phuc's Blog
Go back

Web Push Notification

Edit page

Web Push Notifications or browser notifications are mechanism that browser daemon maintain persistent connection to a cloud push service (e.g., FCM, APNs) to receive near-real-time messages sent from application servers, waking up browser’s Service worker even when the website is closed.

Tip

Looking for a practical implementation guide using Firebase infrastructure? Check out my guide on Firebase Cloud Messaging (FCM) for backend server integration and code examples.

Web Push Notifications

Table of contents

Open Table of contents

Browser support for web push notifications

Note
  • IOS does not support web push notifications yet.
  • On mobile browsers, the support is limited to Android devices only (Chrome, Firefox, Opera).

On the desktop, web push notifications are supported by Chrome, Firefox, Safari, Opera and Edge.

Service worker

Note
  • Push notification is handled by the browser’s service worker.
  • A service worker receives push event when it arrives on the device.

Using service worker to subscribe to certain notifications. The PushManager interface of the Push API provides a way to receive notifications from other servers.


Service Worker Lifecycle & Event Wake-up Flow

To conserve mobile battery, CPU, and RAM, a browser does not keep Service Workers running continuously. Instead, Service Workers are kept in a sleeping/terminated state until an event occurs.

When a push notification arrives:

  1. The OS / Browser daemon receives the incoming TCP push packet over a persistent low-power socket connection.
  2. The Browser Main Process identifies the registered application origin and wakes up (spawns) the Service Worker execution context.
  3. The Service Worker handles the push event, triggers UI notifications via showNotification(), and returns to sleep after the event.waitUntil() promise resolves.
sequenceDiagram
    autonumber
    participant PushServer as Push Service (FCM / APNs)
    participant OS as OS / Push Daemon
    participant Browser as Browser Main Process
    participant SW as Service Worker (JS Context)

    Note over SW: Idle State: Terminated / Sleeping (0% CPU, Saved RAM)
    PushServer->>OS: 1. Push Event Packet (Persistent Connection)
    OS->>Browser: 2. Receive Packet & Dispatch to Browser
    Browser->>Browser: 3. Identify Origin & Target Service Worker
    Browser->>SW: 4. Spawn SW Process & Instantiate V8 Context
    Browser->>SW: 5. Fire `push` Event (self.addEventListener('push'))
    activate SW
    Note over SW: Execute JS Handler & showNotification()
    SW-->>Browser: 6. Promise Resolved (event.waitUntil)
    deactivate SW
    Note over SW: Return to Sleep / Terminated State

Architectural Comparison: Web Push vs. Linux epoll

The mechanism of putting a process to sleep and waking it up only when an event occurs is heavily inspired by kernel-level event multiplexing, specifically Linux epoll.

How Linux epoll Works

Instead of constantly polling sockets in a busy loop (which wastes CPU), a server application (like NGINX or Node.js libuv) delegates socket monitoring to the Linux Kernel via epoll_wait(). The process goes into a blocked/sleeping state (TASK_INTERRUPTIBLE) until an I/O event arrives at the Network Interface Card (NIC).

sequenceDiagram
    autonumber
    participant NIC as Hardware NIC / Network
    participant Kernel as Linux Kernel (TCP Stack & epoll)
    participant Process as User-Space Process (Event Loop)

    Note over Process: Sleeping State: epoll_wait() (Yields CPU)
    NIC->>Kernel: 1. Hardware Interrupt (Packet arrives at Socket FD)
    Kernel->>Kernel: 2. Process Packet & Add FD to epoll Ready List (rdlist)
    Kernel->>Process: 3. Wake Up Process (wake_up_process -> TASK_RUNNING)
    Process->>Process: 4. epoll_wait() returns Ready File Descriptors
    activate Process
    Note over Process: Execute Handler (Read / Write Socket Data)
    Process->>Kernel: 5. Call epoll_wait() again
    deactivate Process
    Note over Process: Yield CPU & Return to Sleep State

Architectural Comparison Table

Both Web Push Service Workers and epoll share the same event-driven, push-based design pattern:

FeatureWeb Push (Service Worker)Linux epoll System Call
Idle StateSW process is terminated / asleepUser process is blocked sleeping on epoll_wait()
Trigger EventIncoming push payload packet from Push GatewayHardware / Network packet on Socket FD
Event DispatcherBrowser Main ProcessLinux Kernel (epoll ready list)
Resource Efficiency0% CPU & 0 RAM when no push notifications0% CPU consuming while waiting for network I/O

How push works

** The three key steps to implementing push are: **

1. Subscribe a user to push notification

const register = await navigator.serviceWorker.register('/sw.js', {
    scope: '/'
});
  1. Getting permission from the user to send them push messages -> The browser will show a popup that asks user to allow the website to receive & show push notifications.
  2. Getting a push subscription object from the browser. It contains all the information we need to send a push message to that user.
// Create a subscription object to send to the server
const subscription = await register.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array('<YOUR_PUBLIC_VAPID_KEY>')
});

2. Understanding VAPID Keys (Voluntary Application Server Identification)

VAPID (RFC 8292) is a security standard using Asymmetric Elliptic Curve Cryptography (EC P-256 / prime256v1) that allows application servers to identify themselves to Push Services (such as FCM, APNs, or Mozilla Push Service) without requiring vendor-specific API keys.

It relies on a keypair consisting of a Public Key and a Private Key:

🔑 VAPID Key Roles: Public vs. Private Key

Key TypeStorage & ExposurePrimary Purpose
Public KeyShared publicly (Client JS / Browser)Identity Registration & Verification
Private KeyStrictly Secret (Server Backend / Env Variables)Digital Signature & Authentication

VAPID Key Pair Authentication Flow

sequenceDiagram
    autonumber
    actor Client as Web Browser (Client)
    participant Server as Application Server (Backend)
    participant PushService as Push Service (FCM / APNs / WNS)

    Note over Server: 1. Generate VAPID Key Pair (EC P-256)<br/>• Public Key (Public)<br/>• Private Key (Secret)

    Client->>Server: 2. Request VAPID Public Key
    Server-->>Client: 3. Return VAPID Public Key
    Client->>PushService: 4. pushManager.subscribe({ applicationServerKey: publicVapidKey })
    Note over PushService: Binds VAPID Public Key to generated PushSubscription Endpoint URL
    PushService-->>Client: 5. Return PushSubscription (Endpoint URL + P256dh / Auth Keys)
    Client->>Server: 6. Save PushSubscription in Backend DB

    Note over Server: --- Push Dispatch Event ---
    Note over Server: 7. Create VAPID JWT & Sign using VAPID Private Key
    Server->>PushService: 8. HTTP POST <Endpoint URL><br/>Header: Authorization: vapid t=<JWT>, k=<VAPID_PUBLIC_KEY>
    
    Note over PushService: 9. Verify JWT Signature using stored Public Key
    alt Signature Valid & Public Key Matches
        PushService-->>Client: 10. Forward Push Payload to Browser / Service Worker
    else Signature Invalid / Mismatch
        PushService-->>Server: 401 Unauthorized / 403 Forbidden Response
    end
Important
  • Never leak your VAPID Private Key to the client side or source control.
  • If your private key is leaked, any attacker could send push notifications to all subscriptions bound to your public key.

Edit page
Share this post:

Previous Post
Understanding RabbitMQ from scratch
Next Post
Entity-Attribute-Value (EAV) in Database Design Pattern

Comments