← PushPig
Web Push (VAPID)
How VAPID and Web Push work — and how to get both without a single line of service-worker code.
Web Push reaches your users right in the browser — even when your site isn't
open. Three standards are at work underneath: the push protocol itself
(RFC 8030), end-to-end encryption of the payload
(RFC 8291) and identification of your server to the push service via
VAPID (RFC 8292). PushPig handles all three — you write no service worker
and manage no VAPID keys.
What is VAPID (RFC 8292)?
VAPID stands for Voluntary Application Server Identification for Web Push. It answers a
single question: who is the server sending this push message? Without VAPID, a push service like
Chrome's or Firefox's accepts messages anonymously — it can't attribute abuse and can't reach you
when something goes wrong. With VAPID your application server signs every delivery using an
ECDSA key pair (P-256, ES256).
What gets signed is a JWT with exactly three relevant claims:
aud — the origin of the push endpoint, e.g. https://fcm.googleapis.com
exp — expiry as a Unix timestamp; per the RFC at most 24 hours in the future
sub — your contact address as a mailto: URI or https: URL,
so the push service operator can reach you
The Authorization header
RFC 8292 defines the vapid auth scheme for this, with two parameters:
t carries the signed JWT, k the matching public key (uncompressed P-256
point, 65 bytes, base64url without padding):
POST /wp/abcd1234 HTTP/1.1
Host: fcm.googleapis.com
Authorization: vapid t=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOi…, k=BKxUj7mSc1nT2vQ3…
Content-Encoding: aes128gcm
TTL: 86400
The JWT itself is header ({"typ":"JWT","alg":"ES256"}), the three claims and the
ES256 signature — each base64url-encoded and joined with dots. The k parameter is the
same public key the push service verifies the signature against.
Older implementations still use the predecessor scheme from the VAPID draft:
Authorization: WebPush <JWT> plus a separate
Crypto-Key: p256ecdsa=<public key> header. Most push services accept both —
vapid t=…, k=… is the standardised form.
The same public key is passed in the browser as applicationServerKey to
pushManager.subscribe(). If the subscription key and the signing key don't match, the
push service rejects the delivery with 403 — the most common failure in hand-rolled
setups.
Generating VAPID keys
A VAPID key is an ordinary ECDSA key pair on the P-256 curve
(prime256v1). For a self-built setup you generate it once and store it on your
application server — with OpenSSL:
openssl ecparam -name prime256v1 -genkey -noout -out vapid_private.pem
openssl ec -in vapid_private.pem -pubout -out vapid_public.pem
The keys are not transmitted as PEM, though, but as base64url without padding:
the public one as an uncompressed curve point (65 bytes, first byte 0x04 —
which is why every VAPID public key starts with a B), the private one as a 32-byte
scalar. The Node package web-push emits both in exactly that encoding:
npx web-push generate-vapid-keys
That's where the actual work begins. The private key has to stay safe server-side, a fresh JWT
must be signed for every push endpoint (exp at most 24 hours in the future), and
the public key must never change: every existing subscription is bound to that
exact applicationServerKey and becomes worthless after a rotation — every subscriber
would have to opt in again.
With PushPig this step disappears entirely. The service holds the key pair, signs every delivery
and serves the public key to the browser — you need neither a VAPID server of your own nor a key
in your deployment.
VAPID is not encryption
A widespread misconception: VAPID authenticates the sender, it does not protect the
content. The payload is encrypted separately per RFC 8291 using
aes128gcm — with the p256dh key and the auth secret from
the recipient's subscription. The browser's push service therefore relays a blob it cannot read
itself.
Concretely, the sender performs an ECDH exchange between a freshly generated
ephemeral key and the recipient's p256dh, derives the content encryption key and
nonce from it via HKDF-SHA256 — salted with the auth secret — and
encrypts the plaintext with AES-128-GCM. The two mechanisms are independent: RFC 8292 decides
whether the push service accepts the message, RFC 8291 decides who can read
it. The encrypted payload may be at most 4,096 bytes.
The specifications in the original
| RFC | Covers |
| RFC 8030 |
Generic Event Delivery Using HTTP Push — the push protocol itself |
| RFC 8291 |
Message Encryption for Web Push — end-to-end encryption of the payload |
| RFC 8292 |
VAPID — application server identification, the vapid auth scheme |
What PushPig takes off your hands
Everything above is why a self-built Web Push setup is more work than expected. With PushPig it's
a single HTTP request:
curl -X POST https://pushpig.de/api/push/send \
-H "X-API-Key: $PUSHPIG_KEY" \
-H "Content-Type: application/json" \
-d '{"channel":"deploys","title":"Deploy finished","body":"v2.4.1 is live"}'
- Key pair generation, rotation and per-endpoint JWT signing — done
- Per-recipient encryption per RFC 8291 — done
- Service worker, web app manifest and the complete PWA setup — shipped
- Expired subscriptions (
404/410) are cleaned up automatically
Supported browsers
- Chrome, Edge, Firefox (desktop & Android) — fully supported
- Safari on macOS — fully supported
- iPhone / iPad (Safari 16.4+) — after „Add to Home Screen" (PWA)
How it works
Users subscribe to a channel in the dashboard or the installed PWA and allow browser
notifications. From then on every message is delivered in real time — encrypted per
RFC 8291 and VAPID-signed per RFC 8292.
iOS in detail
On iPhone and iPad, Web Push only works from a PWA added to the home screen: in Safari use the
share icon → Add to Home Screen, launch the app from there and enable browser
notifications in settings.
What you get
- Title, body and up to two action buttons per message
- Real-time feed in the dashboard via Server-Sent Events
- Per-event delivery status — see how many devices were reached
- Opt-in email fallback when push can't be delivered
Frequently asked questions
What does VAPID stand for?
VAPID stands for Voluntary Application Server Identification for Web Push and is
specified in RFC 8292. It lets an application server voluntarily identify itself to the
browser's push service by signing every delivery with an ECDSA key pair (P-256, ES256).
What does the VAPID Authorization header look like?
RFC 8292 defines the vapid auth scheme with two parameters:
Authorization: vapid t=<JWT>, k=<public key>. The t
parameter carries the ES256-signed JWT with the aud, exp and
sub claims, k carries the matching public key as an uncompressed P-256
point in base64url without padding. Older implementations still use
Authorization: WebPush <JWT> together with a separate Crypto-Key
header.
What is the difference between RFC 8291 and RFC 8292?
RFC 8292 (VAPID) authenticates the sender to the push service. RFC 8291 encrypts the payload
end-to-end with aes128gcm so the push service cannot read the content. The two are independent
of each other and are used together with the push protocol itself, RFC 8030.
Do I need my own VAPID server?
Only for a self-built setup. Then you have to generate the key pair, keep the private key safe,
sign a JWT per push endpoint and leave the public key unchanged. PushPig handles key management,
signing and encryption — you just send an HTTP POST.
What happens if I rotate the VAPID key?
Every existing subscription becomes invalid. A subscription is bound to the
applicationServerKey it was created with; after a rotation the push service rejects
delivery with 403 and every subscriber would have to opt in again.
Does web push work on iPhone?
Yes, from Safari 16.4 — but only from a PWA added to the home screen. In Safari use the share
icon, choose "Add to Home Screen", launch the app from there and allow notifications.
Help & API ·
Pricing ·
Contact ·
Imprint ·
Privacy