A dependency-free Web Push implementation for Node.js (TypeScript-first).
This library focuses on standards-compliant payload encryption + VAPID authentication, and produces request details
that can be sent with Node’s built-in fetch().
- RFC 8188: HTTP Encrypted Content Encoding (record-based framing, per-record nonce derivation).
- RFC 8291: Web Push message encryption (ECDH + auth secret, “WebPush: info” key schedule,
aes128gcm). - RFC 8292: VAPID (JWT ES256) authentication headers.
- Supports both encodings:
aes128gcm(recommended): modern Web Push encoding (RFC 8291 + RFC 8188).aesgcm(legacy): kept for interoperability with older endpoints.
- Pure Node.js
crypto(no external libs). - RFC8188 record framing:
- delimiter rules (
0x01for non-last,0x02for last) - optional final-record padding
- nonce =
baseNonce XOR SEQper record
- delimiter rules (
- Native ES256 JWT signing using Node’s
crypto(JWK-based key objects). - Key generation (
VAPID.GenerateKeys()). - Full validation helpers (
VAPID.Validate.*).
generateRequest()produces{ endpoint, init }forfetch(endpoint, init).- Sets required headers:
TTL,Urgency, optionalTopicContent-Encoding,Content-Type,Content-LengthAuthorization(VAPID or GCM/FCM key when applicable)
- Detects legacy GCM endpoints:
- Uses
Authorization: key=<apiKey>(VAPID not supported on legacy GCM).
- Uses
- Supports FCM endpoints:
- Uses VAPID by default when configured.
- Can fall back to
Authorization: key=<apiKey>if VAPID is disabled and a key is provided.
npm install node-webpushTypeScript is supported out of the box (the package emits .d.ts).
import {WebPush} from "node-webpush";
const webpush = new WebPush({
vapid: {
subject: "mailto:admin@example.com",
publicKey: process.env.VAPID_PUBLIC_KEY!,
privateKey: process.env.VAPID_PRIVATE_KEY!,
},
// Optional: used for legacy GCM/FCM key-based auth fallback
gcm: {apiKey: process.env.GCM_API_KEY ?? null},
});const subscription = {
endpoint: "https://push-service.example/...",
keys: {
p256dh: "<base64url>",
auth: "<base64url>",
},
};
const res = await webpush.notify(subscription, "Hello from WebPush!", {
TTL: 60,
});
console.log("Status:", res.status);import {VAPID} from "node-webpush";
const keys = VAPID.GenerateKeys();
console.log(keys.publicKey);
console.log(keys.privateKey);You typically store these as environment variables:
VAPID_PUBLIC_KEYVAPID_PRIVATE_KEY
type WebPushConfig = {
vapid: {
publicKey: string;
privateKey: string;
subject: string | URL; // must be https: or mailto:
};
gcm?: { apiKey?: string | null };
};Constructing WebPush validates:
- VAPID subject format (
https:ormailto:) - VAPID key sizes and base64url encoding
- GCM/FCM key if provided (must be non-empty)
Returns the request parameters to call fetch() yourself.
const {endpoint, init} = webpush.generateRequest(subscription, "payload", {
TTL: 60,
});
const res = await fetch(endpoint, init);This is useful if you want to:
- inspect headers
- plug into your own HTTP stack
- retry logic / circuit breakers
- log request metadata
Sends the request using fetch().
const res = await webpush.notify(subscription, "hello");default it return the response even if not successful. It can also throw an error if the push service returns a non-2xx response. This can be enabled by:
import {WebPushError} from "./webpush";
try {
const res = await webpush.notify(subscription, "hello", {
throwOnInvalidResponse: true //Add this to the options
});
} catch (error: WebPushError){
console.error(error);
const responseObject = error.response; //<<-- The resulting response object can still be accessed
}Throws
WebPushErrorwhen the push service returns a non-2xx response.This also contains the response but can be handled in the try-catch logic
type GenerateRequestOptions = {
headers?: Record<string, string>;
TTL?: number; // seconds
urgency?: "very-low" | "low" | "normal" | "high";
topic?: string; // base64url <= 32 chars
contentEncoding?: "aes128gcm" | "aesgcm";
// RFC8188 knobs (primarily for advanced use/testing)
rs?: number; // default 4096, must be >= 18
allowMultipleRecords?: boolean; // default false (Web Push wants single record)
finalRecordPadding?: number; // default 0
// Override authentication behavior:
vapidDetails?: WebPushConfig["vapid"] | null;
gcmAPIKey?: string | null;
};aes128gcmis recommended for Web Push.- For Web Push interoperability, leave
allowMultipleRecordsatfalse(default). topicmust use URL-safe base64 characters and be <= 32 chars.
This library follows typical push-service rules:
- Legacy GCM endpoint (
https://android.googleapis.com/gcm/send...)
- Uses
Authorization: key=<gcmAPIKey> - VAPID is ignored (not supported)
- Everything else
- If
vapidDetailsis present: uses VAPID - Else if endpoint is FCM and a key is present: uses
Authorization: key=<gcmAPIKey>
If you want to disable VAPID for a call:
await webpush.notify(subscription, "hello", {
vapidDetails: null,
gcmAPIKey: process.env.GCM_API_KEY!,
});import {WebPush} from "node-webpush";
const webpush = new WebPush({
vapid: {
subject: "https://example.com/contact",
publicKey: process.env.VAPID_PUBLIC_KEY!,
privateKey: process.env.VAPID_PRIVATE_KEY!,
},
});
const {endpoint, init} = webpush.generateRequest(subscription, "ping", {
TTL: 120,
urgency: "high",
});
console.log(init.headers); // inspect headers
const res = await fetch(endpoint, init);
console.log(res.status);import {WebPush, WebPushError} from "node-webpush";
try {
await webpush.notify(subscription, "hello");
} catch (e) {
if (e instanceof WebPushError) {
console.error("Push service rejected request:", e.response.status);
console.error("Response body:", await e.response.text());
} else {
console.error("Unexpected error:", e);
}
}- Node.js with global
fetch(Node 18+ recommended). - TypeScript
target: ES2020works.
- Keep your VAPID private key secret.
- Always validate subscriptions server-side before storing or using them.
- Avoid sending sensitive data in payloads; push payloads can be stored/forwarded by push services.
Apache 2.0 See LICENSE