PHPackages                             bhhaskin/laravel-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. bhhaskin/laravel-webpush

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

bhhaskin/laravel-webpush
========================

Web push notification channel for Laravel. VAPID-signed browser push delivered via Laravel's Notification system.

0.2.0(1mo ago)018MITPHPPHP ^8.1

Since Apr 19Pushed 1mo agoCompare

[ Source](https://github.com/bhhaskin/laravel-webpush)[ Packagist](https://packagist.org/packages/bhhaskin/laravel-webpush)[ RSS](/packages/bhhaskin-laravel-webpush/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (4)Dependencies (8)Versions (4)Used By (0)

laravel-webpush
===============

[](#laravel-webpush)

Web Push notification channel for Laravel. Sends VAPID-signed browser push notifications via Laravel's standard `Notification` system. Subscriptions are polymorphic, so any model (not just `User`) can be notified.

Built on [minishlink/web-push](https://github.com/web-push-libs/web-push-php).

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

[](#installation)

```
composer require bhhaskin/laravel-webpush
```

Publish and run the migration:

```
php artisan vendor:publish --tag=webpush-migrations
php artisan migrate
```

Optionally publish the config:

```
php artisan vendor:publish --tag=webpush-config
```

VAPID keys
----------

[](#vapid-keys)

Generate a keypair once per deployment:

```
vendor/bin/web-push generate-vapid-keys
```

Add to your `.env`:

```
VAPID_SUBJECT="mailto:you@example.com"
VAPID_PUBLIC_KEY="..."
VAPID_PRIVATE_KEY="..."
```

The public key is also served to the browser by `GET /api/push-subscriptions/vapid-key`.

Setup
-----

[](#setup)

Add the `HasPushSubscriptions` trait to any notifiable model:

```
use Bhhaskin\LaravelWebpush\Concerns\HasPushSubscriptions;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;
    use HasPushSubscriptions;
}
```

The trait adds `pushSubscriptions()`, `addPushSubscription()`, `removePushSubscription()`, and `routeNotificationForWebpush()`. The last is what Laravel's notification system calls to resolve delivery targets.

Sending a notification
----------------------

[](#sending-a-notification)

Define `toWebPush()` on your notification and include `'webpush'` in `via()`:

```
use Bhhaskin\LaravelWebpush\Messages\WebPushMessage;
use Illuminate\Notifications\Notification;

class OrderShipped extends Notification
{
    public function __construct(public int $orderId) {}

    public function via($notifiable): array
    {
        return ['webpush'];
    }

    public function toWebPush($notifiable): WebPushMessage
    {
        return WebPushMessage::create('Your order shipped')
            ->body('Tracking is available in your dashboard.')
            ->icon('/icons/package.png')
            ->tag("order-{$this->orderId}")
            ->url("/orders/{$this->orderId}");
    }
}
```

Send it exactly like any other notification:

```
$user->notify(new OrderShipped($order->id));
```

The channel fans out to every `PushSubscription` attached to the notifiable.

Message builder
---------------

[](#message-builder)

`WebPushMessage` is a fluent builder for the payload your service worker receives:

```
WebPushMessage::create('Title', 'Body')
    ->icon('/icons/icon-192.png')
    ->badge('/icons/badge-96.png')
    ->image('/images/hero.png')
    ->tag('unique-tag')          // replaces prior notifications with the same tag
    ->url('/path/to/open')       // read in the SW click handler via event.notification.data.url
    ->requireInteraction()
    ->renotify()
    ->vibrate([200, 100, 200])
    ->action('done', 'Done', '/icons/done.png')
    ->action('snooze', 'Snooze')
    ->withData(['conversation_id' => 42])
    ->ttl(3600)                   // sending option, not payload
    ->urgency('high')
    ->topic('chat-42');
```

`toPayload()` returns the JSON object delivered to the service worker. `getOptions()` returns the Web Push protocol options (`TTL`, `urgency`, `topic`) forwarded to the push service.

HTTP endpoints
--------------

[](#http-endpoints)

When `webpush.routes.enabled` is true (the default), three routes are registered:

MethodPathPurposeGET`/api/push-subscriptions/vapid-key`Returns the public VAPID keyPOST`/api/push-subscriptions`Subscribe the authenticated notifiableDELETE`/api/push-subscriptions`Unsubscribe by endpointPOST body:

```
{
  "endpoint": "https://fcm.googleapis.com/fcm/send/...",
  "keys": { "p256dh": "...", "auth": "..." }
}
```

Configure prefix, middleware, or controller in `config/webpush.php`. Set `routes.enabled` to `false` and mount `PushSubscriptionController` yourself if you need multiple guards or different paths.

Client-side
-----------

[](#client-side)

Subscribe from the browser and post the subscription to `/api/push-subscriptions`:

```
const { vapid_key } = await fetch('/api/push-subscriptions/vapid-key').then(r => r.json());

const registration = await navigator.serviceWorker.register('/sw.js');
await Notification.requestPermission();

const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(vapid_key),
});

await fetch('/api/push-subscriptions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
    body: JSON.stringify(subscription.toJSON()),
});

function urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
    const raw = atob(base64);
    return Uint8Array.from([...raw].map(c => c.charCodeAt(0)));
}
```

A minimal service worker handler for the payload shape emitted by `WebPushMessage::toPayload()`:

```
self.addEventListener('push', (event) => {
    const data = event.data?.json() ?? {};
    event.waitUntil(self.registration.showNotification(data.title ?? '', {
        body: data.body,
        icon: data.icon,
        badge: data.badge,
        image: data.image,
        tag: data.tag,
        actions: data.actions,
        requireInteraction: data.requireInteraction,
        renotify: data.renotify,
        silent: data.silent,
        vibrate: data.vibrate,
        data: { url: data.url, ...(data.data ?? {}) },
    }));
});

self.addEventListener('notificationclick', (event) => {
    event.notification.close();
    const url = event.notification.data?.url;
    if (!url) return;
    event.waitUntil(clients.openWindow(url));
});
```

Expired subscription handling
-----------------------------

[](#expired-subscription-handling)

When a push service returns 404 or 410 for a subscription, the channel dispatches `Bhhaskin\LaravelWebpush\Events\PushSubscriptionExpired` and, unless `webpush.prune_expired` is `false`, deletes the row. Listen for the event if you need to log, notify, or take other action before the row is removed.

Configuration reference
-----------------------

[](#configuration-reference)

See `config/webpush.php` after publishing. Key options:

- `vapid.subject` / `vapid.public_key` / `vapid.private_key`: required
- `model` / `table`: swap the Eloquent model or table name
- `routes.enabled` / `routes.prefix` / `routes.middleware` / `routes.controller`
- `options.TTL` / `options.urgency` / `options.topic`: per-request defaults
- `prune_expired`: delete rows when push services report the subscription is gone

Testing
-------

[](#testing)

```
composer test
```

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance90

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity34

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

Every ~0 days

Total

3

Last Release

51d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/4709778?v=4)[Bryan Haskin](/maintainers/bhhaskin)[@bhhaskin](https://github.com/bhhaskin)

---

Top Contributors

[![bhhaskin](https://avatars.githubusercontent.com/u/4709778?v=4)](https://github.com/bhhaskin "bhhaskin (5 commits)")

###  Code Quality

TestsPest

### Embed Badge

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

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

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k28.4M134](/packages/laravel-cashier)[spatie/laravel-health

Monitor the health of a Laravel application

88011.3M149](/packages/spatie-laravel-health)[laravel-notification-channels/webpush

Web Push Notifications driver for Laravel.

8935.1M24](/packages/laravel-notification-channels-webpush)[fleetbase/core-api

Core Framework and Resources for Fleetbase API

1232.2k16](/packages/fleetbase-core-api)[api-platform/laravel

API Platform support for Laravel

59156.3k10](/packages/api-platform-laravel)

PHPackages © 2026

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