Rotate keys and operate delivery
Rotate VAPID keys
Section titled “Rotate VAPID keys”Generate a new pair. Deploy it as current and retain the previous pair for your chosen grace period.
const push = createPush({ vapid: { publicKey: env.NEW_PUBLIC_KEY, privateKey: env.NEW_PRIVATE_KEY, subject: env.VAPID_SUBJECT, previous: [ { publicKey: env.OLD_PUBLIC_KEY, privateKey: env.OLD_PRIVATE_KEY }, ], },});Old subscriptions are signed with their matching retained key and have needsResubscribe: true. Returning browsers replace them under the current public key. Your callback can remove previousEndpoint and write the replacement in one transaction.
After the grace period, remove the old pair. Remaining old rows report outdated without a network request. Do not rewrite their keyIds. A server cannot rebind an existing browser subscription. For a compromised key, remove it immediately and accept that those browsers must reopen before they can receive again.
Queue large sends
Section titled “Queue large sends”The library has six slots per send call. Multiple simultaneous calls multiply concurrency. Select unique endpoints, chunk them according to your runtime budget, and use your application’s durable queue for large lists.
for (let offset = 0; offset < rows.length; offset += 25) { const selected = rows.slice(offset, offset + 25); const report = await push.send(selected, notification, { ttl: 3600 }); await pruneGone(selected, report); await queueTemporaryFailures(selected, report);}The database and queue helpers above are application code. Map report indexes to the selected chunk, not the original full list. Retry only temporary failures with a bounded application retry policy. An ambiguous network failure can duplicate a notification. A stable notification tag can replace an already displayed notification but is not an exactly-once guarantee.
Use a positive TTL that matches the notification’s usefulness. Short-lived activity may need minutes; a message for an offline user may need hours. TTL defaults to one day. A 2xx confirms push-service acceptance, not a delivery receipt.
Enforce device caps
Section titled “Enforce device caps”Count and limit subscriptions within your authenticated database transaction. Reject excess subscriptions with a 429 Response or remove a caller-chosen old device under the same ownership scope. The library cannot enforce a cap because it does not load or query storage.
Diagnose failures
Section titled “Diagnose failures”| Outcome | Action |
|---|---|
| 401 or 403 | Run doctor. Check key pair, contact subject, token audience, server time, and correct keyId. Never delete the subscription. |
| 404 or 410 | Delete the indexed expired row. |
| 413 | Reduce payload size. The complete envelope must fit 3993 UTF-8 bytes. |
| 429 | The library retries once with capped Retry-After. Queue a later retry if still temporary. |
| 5xx or network | Retry later with a bounded queue policy. Accept duplicate risk. |
outdated |
Retain an old key or wait for the browser to resubscribe. |
invalid-subscription |
Inspect stored data and host policy without printing secret field values. |
not-attempted |
A preceding configuration rejection blocked this origin and key. Fix configuration and resend. |
Log only endpointHash, reason, status, and an application event ID. Provider response bodies and raw fetch errors can include capability URLs and must not be copied into logs.