PHPackages                             mojahed/webpush - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. mojahed/webpush

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

mojahed/webpush
===============

Server-side Web Push (VAPID + RFC 8291 aes128gcm) for PHP 7.2+. Framework-agnostic, zero third-party dependencies. Bundles the mds-webpush client JS.

1.0.0(1mo ago)02MITPHPPHP &gt;=7.2

Since Jul 10Pushed 1mo agoCompare

[ Source](https://github.com/md-mojahed/MdsWebpush)[ Packagist](https://packagist.org/packages/mojahed/webpush)[ RSS](/packages/mojahed-webpush/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependenciesVersions (2)Used By (0)

mojahed/webpush
===============

[](#mojahedwebpush)

Server-side Web Push (VAPID + RFC 8291 `aes128gcm`) for PHP 7.2+.
Framework-agnostic. Zero third-party dependencies. Bundles the `mds-webpush` client JS.

---

Table of Contents
-----------------

[](#table-of-contents)

- [Requirements](#requirements)
- [Installation](#installation)
- [Generating VAPID Keys](#generating-vapid-keys)
- [Quick Start](#quick-start)
- [PHP API Reference](#php-api-reference)
    - [WebPush](#webpush)
    - [Subscription](#subscription)
    - [Response Array](#response-array)
- [Client-Side Setup (mds.webpush.js)](#client-side-setup-mdswebpushjs)
    - [Files](#files)
    - [Register](#register)
    - [Unsubscribe](#unsubscribe)
    - [Config Reference](#config-reference)
    - [Error Codes](#error-codes)
- [Push Payload Fields](#push-payload-fields)
- [Laravel Integration](#laravel-integration)
    - [Migration](#migration)
    - [Routes &amp; Controller](#routes--controller)
    - [Sending a Notification](#sending-a-notification)
    - [Cleaning Up Expired Subscriptions](#cleaning-up-expired-subscriptions)
- [How It Works](#how-it-works)
- [Browser Support](#browser-support)
- [Production Checklist](#production-checklist)
- [License](#license)

---

Requirements
------------

[](#requirements)

RequirementDetailsPHP7.2 or laterext-opensslRequired (signing + AES-128-GCM encryption)ext-curlRecommended for HTTP transport (stream fallback is used otherwise)ext-gmpRecommended for ECDH on PHP 7.2 (bcmath is used as fallback)ext-bcmathAlternative ECDH fallback when gmp is unavailable> On PHP 7.3+ with a standard openssl build, only `ext-openssl` is needed.

---

Installation
------------

[](#installation)

```
composer require mojahed/webpush
```

---

Generating VAPID Keys
---------------------

[](#generating-vapid-keys)

VAPID keys identify your server to the push service (FCM, Mozilla, etc.). Generate them once and store them securely.

```
npx web-push generate-vapid-keys
```

Output:

```
Public Key:  BNcRdreALRFXTkOATJDP...
Private Key: UxLevFltuo37CIQR...

```

Store in your `.env` (Laravel) or equivalent:

```
VAPID_PUBLIC_KEY=BNcRdreALRFXTkOATJDP...
VAPID_PRIVATE_KEY=UxLevFltuo37CIQR...
VAPID_SUBJECT=mailto:you@yourdomain.com
```

> The `subject` must be a `mailto:` address or an `https:` URL. It lets the push service contact you if there is a problem.

---

Quick Start
-----------

[](#quick-start)

```
use Mojahed\WebPush;
use Mojahed\Subscription;

// 1. Create the sender
$push = new WebPush([
    'vapid' => [
        'subject'    => 'mailto:you@example.com',
        'publicKey'  => $_ENV['VAPID_PUBLIC_KEY'],
        'privateKey' => $_ENV['VAPID_PRIVATE_KEY'],
    ],
]);

// 2. Build a subscription from the JSON your browser posted
$subscription = Subscription::fromJson($rawJson);
// — or from an array:
$subscription = Subscription::fromArray([
    'endpoint' => 'https://fcm.googleapis.com/fcm/send/...',
    'keys'     => ['p256dh' => '...', 'auth' => '...'],
]);

// 3. Send a notification
$result = $push->send($subscription, [
    'title' => 'Hello',
    'body'  => 'Your order has shipped.',
    'icon'  => '/images/icon-192.png',
    'url'   => '/orders/42',
]);

if ($result['success']) {
    // HTTP 201 — delivered to the push service
}

if ($result['expired']) {
    // HTTP 404 or 410 — delete this subscription from your database
}
```

---

PHP API Reference
-----------------

[](#php-api-reference)

### WebPush

[](#webpush)

```
new WebPush(array $config)
```

Config KeyTypeDefaultDescription`vapid.subject``string`—`mailto:` or `https:` sender identifier (**required**)`vapid.publicKey``string`—VAPID public key, base64url (**required**)`vapid.privateKey``string`—VAPID private key, base64url (**required**)`ttl``int``2419200`Message Time-To-Live in seconds (max 28 days)`urgency``string``null`One of `very-low`, `low`, `normal`, `high``timeout``int``30`HTTP request timeout in seconds#### `send()`

[](#send)

```
$result = $push->send(
    $subscription,          // Subscription, array, or JSON string
    $payload,               // array (JSON-encoded), string, or null
    $options                // optional per-message overrides: ttl, urgency, topic
);
```

Per-message `$options`:

KeyTypeDescription`ttl``int`Overrides the instance default TTL`urgency``string`Overrides the instance default urgency`topic``string`Collapse key — same topic replaces a queued notification (max 32 chars)#### `quickSend()` — static convenience

[](#quicksend--static-convenience)

```
$result = WebPush::quickSend(
    ['vapid' => [...]], // same config array as the constructor
    $subscription,
    $payload,
    $options
);
```

---

### Subscription

[](#subscription)

Accepts the standard browser subscription shape or a flat array.

```
// From raw JSON (e.g. request body from the browser)
$sub = Subscription::fromJson($request->getContent());

// From an associative array
$sub = Subscription::fromArray([
    'endpoint' => 'https://...',
    'keys'     => ['p256dh' => '...', 'auth' => '...'],
]);

// Flat form (keys at top level, not nested)
$sub = Subscription::fromArray([
    'endpoint' => 'https://...',
    'p256dh'   => '...',
    'auth'     => '...',
]);

// Construct directly with raw bytes or base64url strings
$sub = new Subscription($endpoint, $p256dh, $auth);
```

Keys may be supplied as base64url strings (as the browser provides) or as raw bytes — both are accepted and normalized to raw bytes internally for both `p256dh` and `auth`.

---

### Response Array

[](#response-array)

`send()` returns an associative array:

KeyTypeDescription`success``bool``true` when the push service returned HTTP 201`status``int`Raw HTTP status code`body``string`Response body from the push service`expired``bool``true` on 404 or 410 — subscription should be deleted`rateLimited``bool``true` on 429 — back off and retry`retryAfter``int|null`Seconds to wait before retrying (from `Retry-After` header)`endpoint``string`The subscription endpoint that was targeted```
$result = $push->send($subscription, $payload);

if ($result['expired']) {
    // Remove from DB — this subscription will never work again
    $subscription->delete();
}

if ($result['rateLimited']) {
    $wait = $result['retryAfter'] ?? 60;
    // Re-queue the job to run after $wait seconds
}
```

---

Client-Side Setup (mds.webpush.js)
----------------------------------

[](#client-side-setup-mdswebpushjs)

The `webpush-js/` directory contains the browser-side library. No build step, no dependencies — drop it in and use it.

### Files

[](#files)

FilePurposeWhere to place`mds.webpush.js`Main library — load in your page`/public/js/` or CDN`mds.webpush.sw.js`Service Worker`/public/` **(must be at domain root)**> **Important:** `mds.webpush.sw.js` must be served from your domain root (`/mds.webpush.sw.js`) so its scope covers the entire site. Never place it in a subdirectory.

---

### Register

[](#register)

Call `MdsWebpush.register()` from a user gesture (button click). Browsers block permission prompts that fire automatically on page load.

```

Enable Notifications

document.getElementById('notify-btn').addEventListener('click', function () {
    MdsWebpush.register({
        vapidPublicKey: 'YOUR_VAPID_PUBLIC_KEY',
        subscribeUrl:   '/mds-push/subscribe',
        csrfToken:      document.querySelector('meta[name="csrf-token"]').content,
    });
});

```

What happens internally:

1. Validates config and checks HTTPS + browser support.
2. If permission is already `granted`, registers the Service Worker silently.
3. If permission is `default`, checks whether enough time has passed since the user last dismissed the popup (controlled by `retryAfter`).
4. Shows a customizable trust popup. If the user clicks Allow, triggers the browser permission prompt.
5. On permission granted, subscribes via `PushManager` and POSTs the subscription to `subscribeUrl` — but only if the subscription is new or has changed, to avoid hitting your server on every page load.

---

### Unsubscribe

[](#unsubscribe)

```
MdsWebpush.unsubscribe({
    vapidPublicKey: 'YOUR_VAPID_PUBLIC_KEY',
    unsubscribeUrl: '/mds-push/unsubscribe', // optional — notifies your server
    csrfToken:      document.querySelector('meta[name="csrf-token"]').content,
});
// Returns a Promise
```

This calls `subscription.unsubscribe()` in the browser, optionally POSTs the old endpoint to `unsubscribeUrl`, and clears all local state.

---

### Config Reference

[](#config-reference)

#### Required

[](#required)

KeyTypeDescription`vapidPublicKey``string`Your VAPID public key (base64url)#### URLs

[](#urls)

KeyTypeDefaultDescription`swUrl``string``/mds.webpush.sw.js`Path to the Service Worker file`subscribeUrl``string``/mds-push/subscribe`Server endpoint that saves the subscription`unsubscribeUrl``string|null``null`Server endpoint called on `unsubscribe()` — optional`csrfToken``string|null``null`CSRF token (required for Laravel and similar frameworks)#### Popup

[](#popup)

KeyTypeDefault`popup.title``string``'Stay Informed'``popup.message``string``'Enable notifications to receive important updates instantly.'``popup.okText``string``'Allow Notifications'``popup.cancelText``string``'Not Now'`#### Behavior

[](#behavior)

KeyTypeDefaultDescription`retryAfter``string``'daily'`When to re-show the popup after a dismiss: `'daily'`, `'weekly'`, `'never'``onError``function``console.error`Called with `(code, message)` on any error---

### Error Codes

[](#error-codes)

Available as `MdsWebpush.ERROR_CODES.*`:

CodeWhen`HTTPS_REQUIRED`Page is not served over HTTPS (localhost is exempt)`SW_NOT_SUPPORTED`Browser does not support Service Workers`PUSH_NOT_SUPPORTED`Browser does not support the Push API`PERMISSION_DENIED`User denied the notification permission`SW_REGISTRATION_FAILED`Service Worker failed to register`SUBSCRIBE_FAILED``PushManager.subscribe()` failed`UNSUBSCRIBE_FAILED``unsubscribe()` could not complete`SERVER_FAILED`Server returned an error when saving the subscription`INVALID_CONFIG``vapidPublicKey` is missing or invalid```
MdsWebpush.register({
    vapidPublicKey: '...',
    onError: function (code, message) {
        if (code === MdsWebpush.ERROR_CODES.PERMISSION_DENIED) {
            // Show UI explaining how to re-enable from browser settings
        }
    },
});
```

---

Push Payload Fields
-------------------

[](#push-payload-fields)

Your server sends this JSON as the notification payload. All fields are optional.

```
{
    "title":     "New Message",
    "body":      "You have a new message from Ahmed.",
    "icon":      "/images/icon-192.png",
    "badge":     "/images/badge-72.png",
    "image":     "/images/preview.jpg",
    "url":       "/messages",
    "tag":       "message-thread-1",
    "renotify":  true
}
```

FieldTypeDescription`title``string`Notification title. Defaults to `'Notification'` if omitted`body``string`Notification body text`icon``string`Small icon URL (192×192 recommended)`badge``string`Monochrome badge icon for Android status bar (72×72)`image``string`Large image shown inside the notification`url``string`URL opened when the user clicks the notification. Defaults to `/``tag``string`Collapse key — a new notification with the same tag replaces the previous one`renotify``bool`Re-alert the user when a same-tagged notification replaces a previous onePayload size limit: **~4 079 bytes** (RFC 8291 single-record limit).

---

Laravel Integration
-------------------

[](#laravel-integration)

### Migration

[](#migration)

```
Schema::create('push_subscriptions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->text('endpoint');
    $table->string('p256dh');
    $table->string('auth');
    $table->timestamps();

    $table->unique(['user_id', 'endpoint'], 'unique_user_endpoint');
});
```

### Routes &amp; Controller

[](#routes--controller)

```
// routes/web.php
Route::middleware('auth')->group(function () {
    Route::post('/mds-push/subscribe',   [PushController::class, 'subscribe']);
    Route::post('/mds-push/unsubscribe', [PushController::class, 'unsubscribe']);
});
```

```
// app/Http/Controllers/PushController.php
use App\Models\PushSubscription;
use Illuminate\Http\Request;

class PushController extends Controller
{
    public function subscribe(Request $request)
    {
        PushSubscription::updateOrCreate(
            [
                'user_id'  => auth()->id(),
                'endpoint' => $request->input('endpoint'),
            ],
            [
                'p256dh' => $request->input('keys.p256dh'),
                'auth'   => $request->input('keys.auth'),
            ]
        );

        return response()->json(['success' => true]);
    }

    public function unsubscribe(Request $request)
    {
        PushSubscription::where('user_id', auth()->id())
            ->where('endpoint', $request->input('endpoint'))
            ->delete();

        return response()->json(['success' => true]);
    }
}
```

### Sending a Notification

[](#sending-a-notification)

```
use Mojahed\WebPush;
use App\Models\PushSubscription;

$push = new WebPush([
    'vapid' => [
        'subject'    => config('services.vapid.subject'),
        'publicKey'  => config('services.vapid.public_key'),
        'privateKey' => config('services.vapid.private_key'),
    ],
]);

$subscriptions = PushSubscription::where('user_id', $user->id)->get();

foreach ($subscriptions as $row) {
    $result = $push->send(
        [
            'endpoint' => $row->endpoint,
            'keys'     => ['p256dh' => $row->p256dh, 'auth' => $row->auth],
        ],
        [
            'title' => 'Order Shipped',
            'body'  => 'Your order #' . $order->id . ' is on its way.',
            'icon'  => '/images/icon-192.png',
            'url'   => '/orders/' . $order->id,
        ]
    );

    if ($result['expired']) {
        $row->delete(); // push service says this endpoint is gone
    }
}
```

Add these to `config/services.php`:

```
'vapid' => [
    'subject'     => env('VAPID_SUBJECT'),
    'public_key'  => env('VAPID_PUBLIC_KEY'),
    'private_key' => env('VAPID_PRIVATE_KEY'),
],
```

### Cleaning Up Expired Subscriptions

[](#cleaning-up-expired-subscriptions)

Always delete subscriptions that come back `expired` — HTTP 404 and 410 both mean the push service has permanently invalidated the endpoint.

```
if ($result['expired']) {
    PushSubscription::where('endpoint', $result['endpoint'])->delete();
}
```

---

How It Works
------------

[](#how-it-works)

### Subscription flow

[](#subscription-flow)

```
User clicks button
  → MdsWebpush.register(config)
      → validate vapidPublicKey
      → HTTPS + browser support checks
      → If permission granted → register SW silently → subscribe
      → If permission denied  → fire onError(PERMISSION_DENIED)
      → If permission default:
          → Check localStorage cancel date vs retryAfter
          → Show trust popup
              → "Not Now"  → store cancel date → stop
              → "Allow"    → browser permission prompt
                  → Denied  → store cancel date → fire onError
                  → Granted → register SW → subscribe
                      → POST to server only if subscription is new/changed

```

### Push delivery flow

[](#push-delivery-flow)

```
Server sends push
  → POST encrypted payload to push service endpoint (Google / Mozilla)
      → Push service delivers to browser
          → Browser wakes Service Worker
              → SW receives 'push' event
                  → SW calls showNotification()
                      → User sees notification
                          → User clicks notification
                              → SW focuses existing tab or opens new one

```

### Automatic resubscription

[](#automatic-resubscription)

Push subscriptions are not permanent — the push service can rotate or invalidate them at any time. The Service Worker listens for `pushsubscriptionchange`, creates a fresh subscription, and POSTs the new endpoint to `subscribeUrl`. This works even when no browser tab is open. The VAPID key and server URL needed for this are delivered from the page to the SW via `postMessage` and persisted in IndexedDB.

### Encryption (aes128gcm, RFC 8291)

[](#encryption-aes128gcm-rfc-8291)

For each message the server:

1. Generates an ephemeral P-256 key pair.
2. Derives an ECDH shared secret with the subscriber's `p256dh` key.
3. Applies RFC 8291 §3.4 two-step HKDF to produce the AES-128 content-encryption key and nonce:
    - `PRK_key = HMAC-SHA-256(auth_secret, ecdh_secret)`
    - `IKM = HMAC-SHA-256(PRK_key, "WebPush: info" || ua_public || as_public || 0x01)`
    - `PRK = HMAC-SHA-256(salt, IKM)`
    - `CEK = HMAC-SHA-256(PRK, "Content-Encoding: aes128gcm" || 0x01)[0..15]`
    - `NONCE = HMAC-SHA-256(PRK, "Content-Encoding: nonce" || 0x01)[0..11]`
4. Encrypts the payload with AES-128-GCM.
5. Prepends the RFC 8188 header (`salt || record_size || ephemeral_public_key`).

The VAPID JWT (`Authorization` header) is signed with ES256 and proves the server's identity to the push service.

---

Browser Support
---------------

[](#browser-support)

BrowserSupportedChrome 50+✅Firefox 44+✅Edge 17+✅Safari 16+ (macOS 13+ / iOS 16.4+)✅Opera 42+✅Internet Explorer❌> Safari on iOS requires the site to be added to the Home Screen for push to work on iOS 16.4.

---

Production Checklist
--------------------

[](#production-checklist)

- VAPID keys generated and stored in `.env` (never commit them)
- `mds.webpush.sw.js` placed at domain root (`/public/`)
- `subscribeUrl` route is protected by authentication middleware
- Subscribe endpoint is idempotent (same endpoint can be POSTed multiple times safely)
- Expired subscriptions (404 / 410) are deleted from the database
- HTTPS is enabled on the production domain
- Notification icon is at least 192×192 PNG
- `unsubscribeUrl` endpoint wired up for clean opt-out (optional but recommended)

---

License
-------

[](#license)

MIT

---

Made With ❤️ Love By: [md-mojahed.github.io](https://md-mojahed.github.io)

###  Health Score

33

—

LowBetter than 72% of packages

Maintenance90

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity28

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

48d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/92214734?v=4)[Md Mojahedul Islam](/maintainers/md-mojahed)[@md-mojahed](https://github.com/md-mojahed)

---

Top Contributors

[![md-mojahed](https://avatars.githubusercontent.com/u/92214734?v=4)](https://github.com/md-mojahed "md-mojahed (3 commits)")

---

Tags

phpnotificationsWebPushpush notificationsWeb Pushvapid

### Embed Badge

![Health badge](/badges/mojahed-webpush/health.svg)

```
[![Health](https://phpackages.com/badges/mojahed-webpush/health.svg)](https://phpackages.com/packages/mojahed-webpush)
```

###  Alternatives

[bentools/webpush-bundle

Send push notifications through Web Push Protocol to your Symfony users.

72297.8k](/packages/bentools-webpush-bundle)[pushpad/pushpad-php

Pushpad PHP Library

26153.0k](/packages/pushpad-pushpad-php)[devrabiul/laravel-toaster-magic

Laravel Toaster Magic is a lightweight, flexible toast library for Laravel projects, with no jQuery, Bootstrap, or Tailwind dependency.

22678.5k1](/packages/devrabiul-laravel-toaster-magic)[emuniq/filament-browser-notifications

Zero-config browser push notifications for Filament. Piggybacks on database notifications — every sendToDatabase() automatically triggers a Web Push via VAPID.

134.6k](/packages/emuniq-filament-browser-notifications)[minishlink/web-push-bundle

Symfony Bundle around the WebPush library

60354.5k3](/packages/minishlink-web-push-bundle)[tomatophp/filament-accounts

Manage your multi accounts inside your app using 1 table with multi auth and a lot of integrations

738.6k7](/packages/tomatophp-filament-accounts)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
