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.
- 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-pushThe 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
WebPushinstance 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
TTLper campaign: short for flash sales, long for articles (headers). - Use the
urgencyandtopicoptions where your library version supports them. - Never log full subscription keys alongside personal data.
PHP extensions and common setup errors
| Symptom | Likely cause |
|---|---|
| Very slow encryption | The gmp extension isn’t installed; the library falls back to slower math |
| Composer refuses to install | Missing 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.com | VAPID 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.
Keep reading
Laravel browser push notifications (no Firebase)
laravel-notification-channels/webpush setup, VAPID keys, storing subscriptions and a WebPush notification class.
Web push notifications with Node.js
Install web-push, generate keys, save subscriptions with Express, send with TTL and clean up dead endpoints.
VAPID keys for web push, explained
What the key pair does, how the signed JWT works, how to generate keys and why rotation breaks lists.
The Web Push API in JavaScript: a working example
Register a worker, ask on click, subscribe with VAPID, store the subscription and send — complete code.
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.