Web Push Notifications, VAPID & RFC 8291 Cryptography Studio
An end-to-end cryptographic and protocol architecture studio for Web Push. Generate NIST P-256 keypairs, sign RFC 8292 VAPID authentication JWTs, derive ECDH shared secrets and HKDF encryption keys, assemble RFC 8291 aes128gcm binary records, inspect HTTP/2 push gateway wire frames, and synthesize production Service Workers and backend dispatchers.
Voluntary Application Server Identification (RFC 8292) allows push services to identify your application server without requiring vendor-specific developer accounts. The server signs a short-lived JSON Web Token (JWT) using its private P-256 key, matching the public key supplied during browser subscription.
Passed to pushManager.subscribe({ applicationServerKey: urlBase64ToUint8Array(...) })
Never expose this key to client-side code or public repositories.
RFC 8291 Message Encryption & Binary Record Framing (aes128gcm)
Web Push encrypts arbitrary notification payloads using the user agent's ephemeral ECDH public key (p256dh) and authentication secret (auth). The record is encapsulated in RFC 8188 binary format with an 86-byte header, followed by AES-128-GCM ciphertext and a 16-byte authentication tag.
RFC 8291 Payload Capacity Budget0 / 4,096 Bytes
RFC 8188 / RFC 8291 Binary Record Frame Anatomy
Salt [16B]
rs [4B]
id [1B]
Ephemeral AS Public Key [65B]
Ciphertext + Delimiter
Auth Tag [16B]
Byte Range
Field Name
Length
Semantic Value & Purpose
00 .. 15
salt
16 Bytes
Cryptographically random salt for HKDF PRK derivation
16 .. 19
rs (Record Size)
4 Bytes
0x00 0x00 0x10 0x00 (4,096 Octets in Network Byte Order)
20
idlen (Key ID Length)
1 Byte
0x41 (65 Octets for uncompressed P-256 public key)
21 .. 85
keyid (AS Public Key)
65 Bytes
Ephemeral server public key (0x04 || X || Y)
86 .. N-16
Ciphertext
Variable
AES-128-GCM encrypted payload ending with delimiter 0x02
N-16 .. N
tag (AEAD Tag)
16 Bytes
128-bit authentication tag validating payload integrity
Click 'Encrypt Notification Payload' to execute live Web Crypto AES-128-GCM...
Push Gateway HTTP/2 Dispatch & Wire Inspector
Application servers dispatch encrypted push notifications by issuing an HTTP/2 POST request directly to the subscription endpoint with standard RFC 8030 headers: TTL, Urgency, and Topic.
0 = deliver immediately or discard; max 2,419,200 (28 days).
Replaces queued notifications with matching topic.
POST /fcm/send/sample HTTP/2
Host: fcm.googleapis.com
...
curl -v -X POST ...
Push Service HTTP Response Diagnostics
Status Code
Meaning
Required Backend Action
201 Created
Accepted for Delivery
Message queued or transmitted to client device. Store Location header for delivery status polling if supported.
400 Bad Request
Invalid Payload / Header
Malformed RFC 8188 record, invalid Base64URL encoding, or invalid header value. Inspect payload size and crypto headers.
401 Unauthorized
VAPID Authentication Failure
VAPID token expired, signature verification failed, or aud origin does not match the push gateway. Regenerate JWT.
404 / 410 Gone
Subscription Dead / Expired
MANDATORY: Immediately delete the subscription from your database. The user revoked permission or uninstalled the browser.
413 Payload Too Large
Exceeded 4,096 Octets
Payload exceeds push service capacity. Strip embedded images or move rich metadata to an API fetch inside the Service Worker.
429 Too Many Requests
Rate Limited / Quota Exceeded
Respect the Retry-After header and throttle outgoing worker concurrency with exponential backoff.
Client-Side Registration & Service Worker Implementation
Web Push client lifecycle involves requesting user permission, converting the VAPID public key to a Uint8Array, subscribing via PushManager, and registering resilient push and notificationclick handlers inside the Service Worker.
// 1. Helper to decode VAPID Base64URL string to Uint8Array
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;
}
// 2. Register Service Worker and subscribe to Push
async function subscribeUserToPush(vapidPublicKey) {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
throw new Error('Push notifications are not supported on this browser.');
}
// Request user permission
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
throw new Error('Notification permission denied by user.');
}
const registration = await navigator.serviceWorker.register('/sw.js');
await navigator.serviceWorker.ready;
// Subscribe with VAPID applicationServerKey
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, // Mandatory for Chrome and Safari
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
});
// Transmit PushSubscription to backend database
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
console.log('Successfully registered Web Push subscription:', subscription);
return subscription;
}
iOS / iPadOS 16.4+ Web Push Guardrails:
Safari on iOS only supports Web Push when the website is installed to the Home Screen as a standalone PWA. Web Push cannot be enabled from inside the regular Safari browser tab. Ensure your site includes a valid manifest.json with "display": "standalone".
Deep-dive into the security guarantees of the Web Push protocol and the top 6 operational traps that take down production push notification systems.
1. The Unpadded Base64URL Decoding Trap (DOMException)atob() throws DOMException: The string to be decoded is not correctly encoded if the VAPID public key lacks padding = characters or contains standard Base64 +// characters instead of URL-safe -/_. Always use the canonical urlBase64ToUint8Array() helper with '='.repeat((4 - (str.length % 4)) % 4) padding normalization.
2. The 410 Gone Retrial Poisoning Trap
When a user unregisters notifications or uninstalls the browser, the push service returns HTTP 410 Gone. If your backend worker treats 410 as a retryable transient failure and continues pushing, push services (especially Google FCM and Apple APNs) will flag your VAPID sender IP and apply severe global rate limiting. Every 410 Gone and 404 Not Found must trigger an immediate DB delete.
3. The Missing event.waitUntil() Process Termination Trap
Service Workers in mobile browsers are aggressively paused or terminated to save memory and battery. If you call registration.showNotification() without wrapping the promise inside event.waitUntil(), the mobile browser will terminate the Service Worker thread before the notification IPC completes, causing notifications to disappear silently.
4. The VAPID Audience (aud) Origin Mismatch Trap (401 Unauthorized)
RFC 8292 Section 2 mandates that the aud claim in the VAPID JWT must equal the push service origin (e.g. https://fcm.googleapis.com for FCM, https://web.push.apple.com for Apple, https://updates.push.services.mozilla.com for Firefox). If your backend hardcodes https://fcm.googleapis.com for all endpoints, pushes to Apple or Mozilla users will immediately fail with 401 Unauthorized. You must parse the origin dynamically from new URL(subscription.endpoint).origin.
5. The 4,096-Byte Payload Budget Overflow Trap (413 Payload Too Large)
RFC 8291 limits the total encrypted record body to 4,096 bytes. Remember that the binary frame consumes 86 bytes of header overhead plus 16 bytes of GCM authentication tag (102 bytes total). If your plaintext JSON is larger than 3,993 bytes (e.g. by embedding Base64 image data), the push service returns 413 Payload Too Large. Never embed images in payload; send an image URL instead.
6. The Silent Push Enforcement Failure on iOS Safari
Web standards permit userVisibleOnly: false for silent background data sync pushes. However, WebKit and Chrome strictly enforce userVisibleOnly: true. If an application attempts to call showNotification() conditionally or attempts a silent push on iOS, WebKit revokes the push subscription entirely to prevent background battery drains and stealth tracking.
Frequently Asked Technical Questions
Why does the Web Push protocol (RFC 8030 / RFC 8291) require end-to-end encryption instead of relying on TLS?+
In the Web Push architecture, the push service (e.g. Google FCM, Apple Push Notification service, or Mozilla Autopush) acts as an untrusted intermediate broker between the application server and the user agent. While TLS secures the hop between the application server and the push service, and between the push service and the device, TLS alone would allow the push service provider to inspect, store, or modify all push notification payloads. RFC 8291 mandates end-to-end message encryption using ECDH on NIST P-256, HKDF SHA-256, and AES-128-GCM. Because the cryptographic keys (p256dh public key and auth secret) are generated inside the user agent and never shared with the push gateway, the intermediary can only see encrypted binary octets, guaranteeing zero plaintext exposure to Google, Apple, or intermediate network operators.
How does a mobile or desktop OS maintain an active push notification connection without draining battery?+
Rather than every website maintaining an independent persistent WebSocket or HTTP polling connection, the operating system maintains a single multiplexed, low-power TCP connection with a system-level push gateway (APNs for Apple devices, Google Play Services / FCM for Android, WNS for Windows). This socket utilizes long keep-alive intervals (often 15 to 30 minutes) and cellular network hardware interrupts (such as radio wake-up paging channels). When an application server posts an encrypted push record to the push gateway endpoint, the gateway routes it down this single persistent channel. The OS wakes only the targeted browser process and passes the encrypted payload to the registered Service Worker via an IPC push event, preventing background CPU spinning.
What is the critical difference between RFC 8291 (aes128gcm) and the legacy draft-ietf-webpush-encryption (aesgcm)?+
The initial draft of Web Push encryption used Content-Encoding: aesgcm with headers Crypto-Key and Encryption specifying the salt and server public key. This was found to be brittle and vulnerable to header tampering and stripping by intermediate proxies. In 2017, the IETF ratified RFC 8188 (Encrypted Content-Encoding for HTTP) and RFC 8291, replacing aesgcm with aes128gcm. Under RFC 8291, the 86-byte encryption metadata (16-byte salt, 4-byte record size, 1-byte key ID length, and 65-byte uncompressed server public key) is encapsulated directly into the binary body header rather than HTTP headers. All modern browsers (Chrome 60+, Firefox 55+, Safari 16.4+, Edge 79+) use aes128gcm exclusively.
Why does Safari on iOS and iPadOS strictly require a Progressive Web App (PWA) to be added to the Home Screen for Web Push?+
Apple introduced Web Push support in iOS and iPadOS 16.4 with strict platform guardrails. Unlike desktop Safari or Android Chrome where any HTTPS site can request push permission directly in-browser, iOS and iPadOS require the user to explicitly install the website as a PWA via "Add to Home Screen" (manifest.json with display: standalone). Furthermore, the Notification.requestPermission() prompt must be triggered by a direct user gesture (such as tapping a button inside the PWA). Apple enforced this to prevent mobile web spam, ensure push notifications match native app expectations, and conserve battery by restricting background Service Worker wakeups to user-curated apps.
How should a high-throughput backend handle HTTP 410 Gone and 404 Not Found responses from push services?+
An HTTP 410 Gone (or 404 Not Found) response from a push gateway indicates that the user has revoked notification permissions, uninstalled the browser, or the subscription has reached its lifecycle expiration. Continuing to dispatch push messages to expired endpoints wastes server CPU and bandwidth and degrades server reputation with push gateways (which will eventually throttle or rate-limit the application server with 429 Too Many Requests). Production dispatch workers must immediately mark the subscription as dead in the database and prune it from future broadcast batches. Similarly, HTTP 429 responses must be respected using the Retry-After header with exponential backoff.
How does the Web Push Topic header enable notification collapsing and prevent alert storms?+
The Topic header (RFC 8030 Section 5.4) allows the application server to assign an alphanumeric tag (up to 32 characters) to a push message, acting as a collapse key. If a user device is currently offline or sleeping, and the application server dispatches multiple notifications with the same Topic (for example, Topic: sport-scores-live or Topic: chat-room-104), the push service retains only the most recently received message. When the device reconnects, the user receives a single updated alert rather than being flooded with dozens of stale, obsolete notifications. If Topic is omitted, every push message is queued independently until the TTL expires.