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().
aes128gcm).aes128gcm (recommended): modern Web Push encoding (RFC 8291 + RFC 8188).aesgcm (legacy): kept for interoperability with older endpoints.crypto (no external libs).0x01 for non-last, 0x02 for last)baseNonce XOR SEQ per recordcrypto (JWK-based key objects).VAPID.GenerateKeys()).VAPID.Validate.*).generateRequest() produces { endpoint, init } for fetch(endpoint, init).TTL, Urgency, optional TopicContent-Encoding, Content-Type, Content-LengthAuthorization (VAPID or GCM/FCM key when applicable)Authorization: key=<apiKey> (VAPID not supported on legacy GCM).Authorization: key=<apiKey> if VAPID is disabled and a key is provided.npm install node-webpush
TypeScript is supported out of the box (the package emits .d.ts).
WebPush instanceimport {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_KEYnew WebPush(config)type WebPushConfig = {
vapid: {
publicKey: string;
privateKey: string;
subject: string | URL; // must be https: or mailto:
};
gcm?: { apiKey?: string | null };
};
Constructing WebPush validates:
https: or mailto:)webpush.generateRequest(subscription, payload?, options?)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:
webpush.notify(subscription, payload?, options?)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;
};
aes128gcm is recommended for Web Push.allowMultipleRecords at false (default).topic must use URL-safe base64 characters and be <= 32 chars.This library follows typical push-service rules:
https://android.googleapis.com/gcm/send...)Authorization: key=<gcmAPIKey>vapidDetails is present: uses VAPIDAuthorization: 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);
}
}
fetch (Node 18+ recommended).target: ES2020 works.Apache 2.0 See LICENSE