DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowHalloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PC×
Skip to content
World desk9 min

Web Push from a PHP Backend Without a Vendor: VAPID, ES256, and the Six Ways It Fails Silently

Send browser push notifications from PHP without a notification vendor: store subscriptions, sign VAPID JWTs with ES256, and track down the six layers where delivery fails silently.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A PHP backend can send browser push notifications without a notification vendor. The working pattern has three parts: store each browser’s complete push subscription, encrypt the payload for that one subscription, and send it to the subscription’s push endpoint with a VAPID-authenticated request. The VAPID JWT is signed with ES256, which is ECDSA over the NIST P-256 curve. An HTTP success from the push service means the request was accepted for delivery. It does not prove that the browser received the message or that the user saw a notification. Most silent failures come from one of six places, covered below.

How a message actually travels

Web Push does not connect PHP directly to a browser. Your server hands the message to a push service, which is run either by the browser vendor or by another provider, and the browser pulls it from there. The sequence is:

As an Amazon Associate I earn from qualifying purchases.

  1. The page registers a service worker and calls registration.pushManager.subscribe() with userVisibleOnly: true and your VAPID public key as applicationServerKey.
  2. The browser contacts its push service and returns a PushSubscription containing an endpoint URL and a keys object with p256dh and auth values.
  3. The page posts that subscription to your backend, which stores it as one record.
  4. PHP encrypts the payload for that subscription, signs a VAPID JWT, and POSTs both to the endpoint.
  5. The push service queues the message or forwards it when the device can receive it.
  6. The browser decrypts the payload, starts the service worker, and fires a push event. The worker is responsible for showing anything the user sees.

The endpoint is a capability URL. Anyone who holds it may be able to send to that subscription, so store it like a credential, avoid writing it in full to logs, and never return it in public API responses.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a library for the encryption and request framing rather than writing that code yourself. MDN’s Offline and background operation guide puts it this way: “The app server can use a third-party library such as web-push to take care of the protocol details.” (MDN Web Docs, Offline and background operation)

Two sets of keys that do different jobs

Most configuration errors come from mixing up the VAPID pair with the subscription’s own keys. They are separate:

Key material Created by Where it lives What it does
VAPID private key You, once, when generating the pair Server only, never sent to the browser Signs the ES256 JWT
VAPID public key Derived from the private key Sent to the browser as applicationServerKey; sent with each request in the k parameter Identifies your application server to the push service
Endpoint The browser’s push service Your database Destination URL for the POST
p256dh The browser, per subscription Your database The subscriber’s public key used to encrypt the payload
auth The browser, per subscription Your database Authentication secret mixed into payload encryption

Keep the VAPID pair stable. Subscriptions are bound to the public key they were created with, so rotating the pair generally means every browser must subscribe again. Do not substitute p256dh or auth for VAPID keys, and do not use the VAPID private key for anything else.

ES256 and what the VAPID JWT must contain

VAPID (Voluntary Application Server Identification, defined in RFC 8292) lets your server identify itself to the push service. The identity is a signed JWT, and RFC 8292 requires the signature to use ECDSA over P-256 with SHA-256, identified as ES256. The JWT carries three claims:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • aud: the origin of the push endpoint, meaning scheme and host only, such as https://push.example for an endpoint at https://push.example/send/abc. A path in the audience is a common reason for rejection.
  • exp: an expiry time as a Unix timestamp, no more than 24 hours after the request is sent.
  • sub: a contact URI that must use mailto: or https:.

The JOSE header is {"typ":"JWT","alg":"ES256"}. The request then carries the token in an Authorization header with the public key:

Authorization: vapid t=<JWT>, k=<VAPID public key, Base64URL>

One detail trips up hand-rolled signers. OpenSSL’s openssl_sign() returns ECDSA signatures in ASN.1 DER form, but a JWS ES256 signature must be the fixed-length 64-byte concatenation of R and S. A DER signature is malformed for this purpose, and the push service will reject it.

PHP setup and sending

Runtime requirements

  • PHP 8.2 or newer, according to the current README of the minishlink/web-push project.
  • The mbstring, curl, and OpenSSL extensions, with OpenSSL built for elliptic-curve operations.
  • bcmath and/or gmp are optional performance aids.
  • Composer’s autoloader loaded before the library is used.

Requirements change between library releases. Older PHP versions can use older release lines of the library, so check the README for the version you pin rather than assuming the current requirement applies to your runtime. Install with:

composer require minishlink/web-push

Storing the subscription

Save endpoint, p256dh, and auth together as one record, and size the database columns for the full values. The library’s README names undersized database fields for authentication data as a common operational problem. Refresh the record each time the page re-subscribes, and delete it when the push service reports the subscription gone.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Subscribing in the browser

Pass the VAPID public key to subscribe() in the form the browser’s Push API accepts; most implementations decode the Base64URL string into bytes first with a helper of their own. Then send the subscription to your server. JSON.stringify() on a PushSubscription produces the endpoint and keys in the shape your backend needs.

const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY), // your helper
});
await fetch('/push/subscribe', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify(subscription),
});

Sending from PHP and checking the result

<?php
require __DIR__ . '/vendor/autoload.php';

use MinishlinkWebPushSubscription;
use MinishlinkWebPushWebPush;

$webPush = new WebPush([
    'VAPID' => [
        'subject'    => 'mailto:[email protected]',
        'publicKey'  => getenv('VAPID_PUBLIC_KEY'),
        'privateKey' => getenv('VAPID_PRIVATE_KEY'),
    ],
]);

// $row is one stored subscription: endpoint, p256dh, auth
$subscription = Subscription::create([
    'endpoint'        => $row['endpoint'],
    'publicKey'       => $row['p256dh'],
    'authToken'       => $row['auth'],
    'contentEncoding' => 'aes128gcm',
]);

$report = $webPush->sendOneNotification(
    $subscription,
    json_encode(['title' => 'Order shipped', 'body' => 'Track it in your account.'])
);

if ($report->isSuccess()) {
    // Accepted by the push service. Record the status only.
} elseif ($report->isSubscriptionExpired()) {
    // Delete this subscription row.
} else {
    error_log('Web Push failed: ' . $report->getReason());
}

Keep keys in environment variables or a secret store rather than in source files. Log the status and reason, never the full endpoint. The payload content coding must match the Content-Encoding header, which is aes128gcm in the usual case, and the library handles that pairing when you let it build the request.

Reading push-service responses

Response Typical meaning What to check
201 Created The push service accepted the message for delivery (RFC 8030) Store the status; delivery to the device is a later stage
400 The request is malformed Headers, including the TTL header, and the encryption framing
401 or 403 The VAPID authorization was rejected Key pair match, JWT audience, ES256 signature format
404 or 410 The subscription no longer exists Delete the stored record and resubscribe on the next visit
413 The payload is too large Shorten the payload; push services accept at least 4,096 bytes (RFC 8030)
429 The sender is rate-limited Back off and retry later

These meanings describe the HTTP status categories the protocol uses; the push service’s own error bodies can add detail, so keep them in your logs.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Six places a failure can hide

“Silent” here means one of two things: your code reports a successful send, or the user sees no notification. Web Push does not suppress every error, so each failure below leaves some evidence somewhere. The six layers follow the order a message passes through the system. They are a practical way to organize the failure points in the specifications and MDN’s guides, not an official checklist.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

1. The browser never created a working subscription

  • Push subscriptions require a secure context (HTTPS, or localhost during development) and an active service worker.
  • Notification permission may be denied. Check Notification.permission in the page before subscribing.
  • subscribe() can reject. Catch the error and record err.name and err.message in the browser.
  • Omitting userVisibleOnly: true causes the call to fail in browsers that require it.

Look here first. Browser-side errors appear before any PHP log line exists.

2. The subscription was created with a different VAPID public key

If the browser subscribed with one applicationServerKey and your server signs with a different private key, authorization fails. Compare the key in PushSubscription.options.applicationServerKey in the browser with the public key your server is configured to use. Both must come from the same pair.

3. The VAPID JWT is malformed or aimed at the wrong audience

  • The aud claim must be the endpoint’s origin, with no path.
  • The signature must be ES256 in raw R||S form, not DER.
  • The exp claim must fall within 24 hours of the request. A server clock running fast or slow can make a valid expiry look wrong, so check the clock too.
  • The sub claim must be a mailto: or https: URI.

4. The saved subscription is stale or incomplete

A truncated auth value or a missing p256dh produces a message the browser cannot decrypt, which looks like silence to the user. Verify that the stored values match a freshly serialized subscription. Replace the record when the page re-subscribes, and remove endpoints the push service reports as expired or gone.

The pushsubscriptionchange event can signal refresh or revocation, but MDN marks it as not available in some widely used browsers. Use it as one reconciliation path, never the only one.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

5. Encryption or runtime setup is wrong

  • Confirm the p256dh and auth values reached PHP intact, with no double-decoding or whitespace changes.
  • Confirm the required extensions are loaded in the same PHP binary your web server uses. CLI and FPM often differ.
  • If the library throws a TLS error, fix the certificate trust store or CA bundle. Disabling peer verification may make the error go away, but it leaves the connection open to interception, so do not use it as the fix.
  • Catch exceptions and log the message alongside the status code.

6. The push event arrives but no notification appears

The worker must register a push listener, parse event.data, and keep the event alive until the notification is created. Call event.waitUntil() around registration.showNotification(). Calling event.data.json() on a payload that is not JSON throws, so guard it.

self.addEventListener('push', (event) => {
  const data = event.data ? event.data.json() : {};
  event.waitUntil(
    self.registration.showNotification(data.title || 'Update', {
      body: data.body || '',
    })
  );
});

The Push specification allows a “silent” push that displays nothing. MDN reports that browsers do not support that behavior because of privacy concerns, so for user-facing notifications, every push should result in a visible notification.

Troubleshooting order

  1. In the browser, confirm HTTPS or localhost, an activated service worker, granted notification permission, and a current subscription.
  2. Compare the subscription’s application-server key with the server’s VAPID public key, and confirm the matching private key is the one signing the JWT.
  3. Compare the stored endpoint, p256dh, and auth with a freshly serialized subscription, without printing the endpoint in logs.
  4. Send one test message. Capture the status code, response body, and exception from the library.
  5. Add logging to the worker’s push handler and confirm the showNotification() promise resolves.
  6. Trigger a resubscription and confirm the server record is replaced rather than duplicated.

Work through the list in order. A failure at an earlier step makes the later steps look broken.

Choosing your implementation approach

There is no vendor comparison to make for a self-hosted setup, so decide on these points instead: whether you rely on the library’s protocol handling or maintain your own; whether your PHP runtime meets the library’s current requirements; how subscription records are refreshed and expired endpoints removed; what your telemetry records (ideally the push-service status and the worker’s handling separately); and whether every push produces a confirmed notification.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.