The Laravel web push channel hides VAPID keys, expired endpoints and PSR-18 differences
Webpush notifications channel for Laravel.
At a glance
- What is it?
- laravel-notification-channels/webpush adds browser push notifications to Laravel's notification system, generating VAPID keys through an artisan command, cleaning up expired subscriptions automatically, and choosing between the Laravel HTTP client and a Guzzle fallback depending on the Laravel version. The Declarative Web Push path is the newest feature and the least settled.
- Who is it for?
- Adopt this package if you are on Laravel and want browser push through the notification system you already use, especially if you want expired push endpoints removed automatically rather than accumulating dead subscriptions.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 45 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A notification channel that fits the via() method you already write
This package makes it easy to send web push notifications with Laravel. Its whole purpose is to slot into the notification system the framework already has, so a push message is delivered the same way as a mail or a database notification.
The integration surface is a channel class and a message class. In your notification, the via method returns the channel, and the message is built in a method named after it.
use Illuminate\Notifications\Notification;
use NotificationChannels\WebPush\WebPushMessage;
use NotificationChannels\WebPush\WebPushChannel;The message builder is chainable, and the README shows the options that matter.
->action('View account', 'view_account')
->options(['TTL' => 1000]);The fuller list of chainable methods is visible in the commented lines of the same example: badge, dir, image, lang, renotify, requireInteraction, tag and vibrate, plus a data payload for passing your own values through to the service worker. The feature list describes the same set as title, body, icon, actions, TTL and more.
So the shape of the work for an existing application is small. Import two classes, return the channel from via, and return a message from toWebPush. Everything about browser support and authentication is handled by the package and the push service, not by your code.
VAPID keys come from an artisan command, and Safari needs a subject
Browser authentication is the part that trips people up, and the package handles the key generation with one command.
php artisan webpush:vapidThe command sets VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY in your .env file. The public key is what you hand to the browser as the applicationServerKey when using the Push API, and the private key stays on the server.
The README is emphatic about one thing: these keys must be safely stored and should not change. That is the whole subscription model. A browser subscription is bound to a key pair, so rotating the keys invalidates every subscription you have collected, and every device silently stops receiving.
Safari and iOS add a requirement that is easy to miss. If targeting Safari or iOS, especially after 2023, you must include the VAPID_SUBJECT variable in your .env file, and it should be a valid URL such as https://example.com or a mailto: address. Apple rejects requests with a BadJwtToken error if VAPID_SUBJECT is missing or invalid, and the domain in it must be a valid, existing top-level domain.
Browser support itself is broad. The package works with all major browsers that implement the Push API, including Chrome, Firefox, Edge and Safari.
None of this is deep protocol work, and that is the point. VAPID is the part of web push that every implementer has to get right, and the package gives you the keys, the artisan command and the failure mode to watch for, in three paragraphs of documentation.
Expired endpoints are deleted on send, not by a cleanup job
This is the feature that separates a maintained channel package from a thin wrapper, and it is worth understanding how it works.
Push subscriptions have a lifecycle. When a browser unsubscribes, or a user clears site data, or the push service decides the endpoint is dead, sending to it fails. The failure is reported back to the sender, and the push library this package builds on exposes that as a report.
The package detects expired endpoints using the expired flag of Minishlink\WebPush\MessageSentReport, and when it sees one while sending a push message, it automatically deletes the expired subscription. There is no cleanup command to schedule and no cron job to remember.
That design has a consequence worth noting. Cleanup happens as a side effect of sending, so a table of stale subscriptions for a user who never receives a notification will keep its dead rows until some other message triggers a send to that endpoint. For an application where every user receives regular notifications, that is fine and the table stays clean. For an application with an irregular notification pattern, some dead rows will persist longer.
The README also documents a schema change that catches people upgrading. If you installed the package before endpoint URLs were widened to 1024 characters, you should publish the migrations again and run php artisan migrate to apply increase_push_subscriptions_endpoint_length. The reason given is concrete: some push services, for example Microsoft WNS, may generate endpoints longer than 500 characters.
So the model is a per-notifiable subscription table maintained by the framework, cleaned opportunistically by the package.
Two HTTP clients, because Laravel 13 changed the rules
The HTTP section is short but it contains the sort of version-specific detail that breaks silently.
On Laravel 13, push notifications use Laravel's HTTP client, and Laravel 13.13 or later is required. The consequence for your application is that global HTTP middleware, request logging and Http::fake() all apply to web push requests. If you have a test suite that asserts on HTTP calls, or middleware that adds headers or logs outbound requests, push delivery is now inside that path.
Laravel 12 uses a Guzzle client with the same timeout and the same client_options configuration. The README explains the fallback exists because earlier Laravel HTTP clients cannot be safely used as PSR-18 clients.
The timeout is 30 seconds by default, and the client_options come from the published webpush configuration file. That configuration file is published with a tag, so you can adjust it for your environment.
php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="config"This is a real difference between the two supported majors, and it is the kind of thing that determines whether an existing test suite passes after an upgrade. If your tests assert on HTTP traffic, upgrading from Laravel 12 to 13 changes what those assertions see for push notifications.
The wider point is that the package pins itself to a moving target. It tracks Laravel majors, and where the framework's HTTP layer changes shape, the package changes with it. That is the right behaviour for a notification channel, and it is also a coupling you should know about before pinning versions.
Subscriptions live on your notifiable model through a trait
The subscription storage is handled by a trait you add to your model, which keeps the package out of your migrations and schema decisions beyond the one it publishes.
use NotificationChannels\WebPush\HasPushSubscriptions;
class User extends Model
{
use HasPushSubscriptions;
}With the trait in place, the package offers convenient methods to save, update and delete push subscriptions directly on your notifiable models. The migration is published and run as usual.
php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="migrations"php artisan migrateThat ordering matters. The trait expects the table to exist, and the table is created by the published migration, so publishing and migrating before you send anything saves a confusing failure.
Because the data lives on the notifiable model rather than in a package-owned table, the design stays out of the way of your own schema decisions. A user has zero or more subscriptions in the same way they would have any other related records, and the association is the usual one.
The installation path overall is the standard Composer one.
composer require laravel-notification-channels/webpushThe package is on Packagist under laravel-notification-channels/webpush, and the repository carries a composer.json, a config/ directory, a migrations/ directory, a src/ directory, tests/, a phpunit.xml.dist, a phpstan.neon with a baseline, a rector.php configuration and a CHANGELOG.md. The version numbers follow the major Laravel release, with 13.0.0 and 13.0.1 both published on 2026-09-08 and 12.1.0 on 2026-08-17.
Declarative Web Push is the newest feature and the least settled
The second message class, DeclarativeWebPushMessage, is worth a separate look because the README is unusually careful about it.
Declarative Web Push messages are a specification from the W3C Push API that aim to reduce the complexity of using push on the web in general and address some challenges of generic web push notifications, like privacy concerns and battery life on mobile, by making a client-side service worker optional while remaining fully backwards compatible.
The mechanics differ from the generic message. The action method takes a third argument, a URL the browser opens.
->action('View account', 'view_account', 'https://myapp.com/accounts')And the message class itself is DeclarativeWebPushMessage rather than WebPushMessage.
The caveat is stated plainly at the top of that section: the specification for Declarative Web Push messages is still evolving and may change in the future, and browser support for this functionality is currently limited and may vary across platforms.
That is the honest position, and it should shape how you plan. If your notification payload needs to reach a browser without the user having granted service worker permission, this is the path. If it needs to work everywhere today, use the generic WebPushMessage class and accept the service worker requirement.
The rest of the feature list is conventional and complete: integration with Laravel's notification system, multiple browser support, VAPID support, flexible message options, subscription management, automatic expired subscription cleanup, and a publishable configuration file.
Where this package is the wrong choice
Three situations rule it out, and they are worth checking before you write any code.
The first is a Laravel version below 12. The README documents the HTTP client behaviour for Laravel 13 and Laravel 12 only, and the release numbers track Laravel majors, so there is no supported path for older majors.
The second is a delivery requirement that includes Safari or iOS without any willingness to set VAPID_SUBJECT. If your .env cannot carry a valid URL or mailto: address for the subject, those devices will reject your pushes with a BadJwtToken error, and no amount of message configuration changes that. The README makes it a must for Safari and iOS, especially after 2023.
The third is Declarative Web Push as a hard requirement. If the whole design depends on push without a service worker and without user permission, you are building on a specification that the README says is still evolving, with browser support that is currently limited and may vary across platforms. That is a bet on the spec, not on the package.
The alternative to this package is to skip Laravel's notification layer and drive the underlying push library yourself. The package depends on Minishlink\WebPush, and its own README references that library's MessageSentReport for the expired flag, so the same underlying engine is available directly. The difference in approach is that the direct route leaves you responsible for subscription storage, VAPID key handling, expired endpoint cleanup, the Laravel 12 versus 13 HTTP client choice, and the service worker integration, all of which this package wraps in a trait, two artisan commands and one channel class.
That is the trade. The package is small and its scope is clear, so for a Laravel application already using notifications it is the shorter path. For an application with its own subscription model, or one that wants to avoid a package dependency in the notification path, calling the library directly is a reasonable alternative.
On the licence, it is MIT, and the repository carries a LICENSE.md file. On maintenance, the last push was on 2026-09-08, the same day as the 13.0.0 and 13.0.1 releases, and the repository is not archived. The releases are frequent enough that staying on the current major is the sensible choice rather than pinning an old point release.
Editorial conclusion
Adopt this package if you are on Laravel and want browser push through the notification system you already use, especially if you want expired push endpoints removed automatically rather than accumulating dead subscriptions. Do not adopt it if you are below Laravel 12, if you need Safari or iOS delivery without setting VAPID_SUBJECT, or if Declarative Web Push is a requirement rather than a bonus, because the README states browser support for it is currently limited and may vary across platforms. Before shipping, run php artisan webpush:vapid once and store the keys, because the README says they must be safely stored and should not change, then confirm your push service generates endpoints inside the widened 1024 character column.
Frequently asked questions
How do I install the Laravel webpush notification channel?
Install it with Composer using composer require laravel-notification-channels/webpush, add the NotificationChannels\WebPush\HasPushSubscriptions trait to your model, publish the migration, and run php artisan migrate.
How do I generate VAPID keys for Laravel web push?
Run php artisan webpush:vapid, which sets VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY in your .env file. The public key becomes the applicationServerKey on the browser side, and the README says the keys must be safely stored and should not change.
Why do my Safari and iOS push notifications fail with BadJwtToken?
Apple rejects requests when the VAPID_SUBJECT variable is missing or invalid. For Safari or iOS, especially after 2023, you must set VAPID_SUBJECT in your .env file to a valid URL such as https://example.com or a mailto: address, using a domain with a valid, existing top-level domain.
How does the Laravel webpush package handle expired push subscriptions?
When sending to an expired endpoint, the package detects the expired flag of Minishlink\WebPush\MessageSentReport and automatically deletes that subscription, so no separate cleanup job is needed.
Which HTTP client does the Laravel webpush package use?
On Laravel 13, which requires version 13.13 or later, push notifications use Laravel's HTTP client, so global HTTP middleware, request logging and Http::fake() apply. Laravel 12 uses a Guzzle client instead, because earlier Laravel HTTP clients cannot be safely used as PSR-18 clients.
What is Declarative Web Push in this Laravel package?
It is a message class that supports Declarative Web Push messages from the W3C Push API specification, which makes a client-side service worker optional while staying backwards compatible. The README warns that the specification is still evolving and browser support is currently limited.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/laravel-notification-channels-webpush)Was this useful?