Skip to content
Developers · 8 min read

Web push notifications in PHP

minishlink/web-push is the standard PHP library for Web Push — and the one Pusharoo’s own sender is built on. Here’s a complete, production-minded setup.

Updated By the Pusharoo team
In short
  • composer require minishlink/web-push installs the library.
  • VAPID::createVapidKeys() generates your key pair once.
  • queueNotification() + flush() sends in batches and returns a report per subscription.
  • Delete subscriptions whose report says isSubscriptionExpired().

Install

composer require minishlink/web-push

The library needs a modern PHP 8 version with the openssl, curl and mbstring extensions (and gmp is recommended for speed). Check its README for the exact versions supported by the release you install.

Create VAPID keys once

<?php require 'vendor/autoload.php'; use Minishlink\WebPush\VAPID; print_r(VAPID::createVapidKeys()); // ['publicKey' => '...', 'privateKey' => '...']

Store both in environment variables; ship only the public key to the browser (VAPID explained).

Store subscriptions

<?php // subscribe.php — receives JSON from pushManager.subscribe() $sub = json_decode(file_get_contents('php://input'), true); if (!isset($sub['endpoint'], $sub['keys']['p256dh'], $sub['keys']['auth'])) { http_response_code(400); exit; } $stmt = $pdo->prepare('INSERT INTO push_subs (endpoint, p256dh, auth) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE p256dh = VALUES(p256dh), auth = VALUES(auth)'); $stmt->execute([$sub['endpoint'], $sub['keys']['p256dh'], $sub['keys']['auth']]); http_response_code(201);

Queue, flush and clean up

<?php use Minishlink\WebPush\WebPush; use Minishlink\WebPush\Subscription; $webPush = new WebPush(['VAPID' => [ 'subject' => 'mailto:push@yoursite.com', 'publicKey' => getenv('VAPID_PUBLIC_KEY'), 'privateKey' => getenv('VAPID_PRIVATE_KEY'), ]], ['TTL' => 3600]); $payload = json_encode(['title' => 'Back in stock', 'body' => 'Linen Shirt, size M', 'url' => '/p/linen-shirt']); foreach ($pdo->query('SELECT endpoint, p256dh, auth FROM push_subs') as $row) { $webPush->queueNotification(Subscription::create([ 'endpoint' => $row['endpoint'], 'keys' => ['p256dh' => $row['p256dh'], 'auth' => $row['auth']], ]), $payload); } foreach ($webPush->flush() as $report) { if ($report->isSubscriptionExpired()) { $pdo->prepare('DELETE FROM push_subs WHERE endpoint = ?')->execute([$report->getEndpoint()]); } }

Running it at scale

  • Send from a CLI worker or cron job, not inside a web request.
  • Flush in batches (for example a few hundred at a time) to limit memory.
  • Reuse one WebPush instance per batch so connections are reused.
  • Log the report reasons for non-expired failures and retry 429/5xx later.

A minimal table

CREATE TABLE push_subs ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, endpoint VARCHAR(700) NOT NULL, p256dh VARCHAR(120) NOT NULL, auth VARCHAR(40) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uniq_endpoint (endpoint(500)) );

Endpoints can be long, so index a prefix. Add columns for language, timezone or a user ID if you want to segment (segmentation ideas).

Payload and options

  • Keep the JSON payload small: title, body, URL, icon and image URL — the encrypted message must fit in about 4 KB.
  • Set TTL per campaign: short for flash sales, long for articles (headers).
  • Use the urgency and topic options where your library version supports them.
  • Never log full subscription keys alongside personal data.

PHP extensions and common setup errors

SymptomLikely cause
Very slow encryptionThe gmp extension isn’t installed; the library falls back to slower math
Composer refuses to installMissing mbstring, curl or openssl, or a PHP version below the package’s requirement
“Unable to create the key”OpenSSL can’t find its configuration (common on Windows dev machines)
403 from fcm.googleapis.comVAPID keys differ from the ones used when the browser subscribed

Check the package’s composer.json for the current PHP requirement before upgrading a server.

Cron or queue worker?

For a few thousand subscribers, a cron job that loads a batch, queues notifications and calls flush() is enough. For larger lists or frequent sends, a long-running queue worker (Laravel queues, Symfony Messenger, Supervisor-managed scripts) avoids reconnecting for every batch and lets you resume after a crash. Save progress after each batch — the sending loop Pusharoo runs records its position so an interrupted send continues without duplicates, and that pattern is worth copying.

Use the report for cleanup and stats

foreach ($webPush->flush() as $report) { $endpoint = $report->getEndpoint(); if ($report->isSuccess()) { $stats['delivered']++; continue; } if ($report->isSubscriptionExpired()) { $db->prepare('DELETE FROM subs WHERE endpoint = ?')->execute([$endpoint]); continue; } error_log("push failed {$report->getReason()}"); }

isSubscriptionExpired() covers 404 and 410; everything else is worth logging with the push service host so you can spot problems with one browser.

The hosted shortcut

Pusharoo’s sender uses this same library, plus a queue, segments, schedules, automations, signed webhooks and automatic cleanup — free with no limits. If you just need notifications on a PHP site (WordPress, Laravel or plain PHP), install sw.js and the snippet instead (setup guide), or see Laravel push notifications.

Questions

Is minishlink/web-push free to use?

Yes, it’s open source under the MIT license.

Does PHP web push need Firebase?

No. The library sends standard Web Push with VAPID to every browser’s push service.

Why is sending slow?

Each notification is encrypted individually. Install the gmp extension, batch sends and run them in a background worker.

Can I send web push from shared hosting with PHP?

Often yes, if the host allows Composer packages and outbound HTTPS, and has the required PHP extensions. Very large lists are better sent by a queue worker than a cron job on shared hosting.

How do I set TTL and urgency with minishlink/web-push?

Pass default options to the WebPush constructor or per notification, for example ['TTL' => 3600, 'urgency' => 'normal', 'topic' => 'sale']. The library sends them as the standard TTL, Urgency and Topic headers.

More in Developers: Service worker push events, done right · The Web Push protocol, explained · Web push notifications in React and Next.js · Declarative Web Push, explained · Firebase web push vs. standard web push · Web push in Python with pywebpush · Django push notifications to the browser · Web push notifications in Ruby on Rails · Web push in Go with webpush-go · Angular push notifications with SwPush · Push notifications in Vue 3 and Nuxt · Push notifications in SvelteKit · All guides

Free for life. No limits.

Unlimited subscribers, campaigns and automations. No badges. About five minutes to install.