Skip to content

Notifications · Open source

Laravel WebPush Kit

The browser side of Laravel web push: a subscribe button with a two-step permission prompt, a service worker, and routes that save each user's subscription. Tested in Chrome so far.

Laravel 12 and 13 PHP 8.3+ v0.1.0 Free · MIT
Laravel WebPush Kit: The browser side of Laravel web push: a subscribe button with a two-step permission prompt, a service worker, and routes that save each user's subscription. Tested in Chrome so far.

What it solves

laravel-notification-channels/webpush sends web push from Laravel, but everything in the browser is left to you: the service worker, the Push API subscribe and unsubscribe calls, the permission prompt, saving the subscription for the signed-in user, and keeping it alive when the browser rotates it.

Ask for permission on page load and many users click Block, and a blocked site cannot ask again.

This package builds on laravel-notification-channels/webpush; it is not made or endorsed by the laravel-notification-channels maintainers. It adds a Blade component that renders a small web component: the page asks first, and only Allow opens the browser's own permission dialog. The subscription is saved and deleted through the channel's own updatePushSubscription() and deletePushSubscription(), and a service worker served from the site root shows the channel's normal and declarative messages. php artisan push:test sends a real push and shows what each push service answered.

  • Blade subscribe button for signed-in users with a two-step prompt: a card you write first, the browser dialog only after Allow
  • States for subscribed (with Turn off), blocked, no push support, no HTTPS, and an Add to Home Screen note for iPhone and iPad (untested)
  • Service worker at /webpush-sw.js that shows WebPushMessage and DeclarativeWebPushMessage pushes and sends the click to data.url or navigate
  • Saves, deletes and rotates subscriptions through the channel's updatePushSubscription() and deletePushSubscription()
  • Works in Livewire and across wire:navigate, with webpush-kit:subscribed and webpush-kit:unsubscribed browser events
  • php artisan push:test with a per-push-service result table, VAPID key and subject checks, and a php artisan about entry
An Acme Shop account page with a Notifications card showing the two-step prompt: the heading Order updates?, a short line about what the notifications are for, and Allow and Not now buttons. The browser's own permission dialog only appears after Allow.
An Acme Shop account page with a Notifications card showing the two-step prompt: the heading Order updates?, a short line about what the notifications are for, and Allow and Not now buttons. The browser's own permission dialog only appears after Allow.

Install

Requires PHP 8.3+ and Laravel 12 and 13.

composer require amjadiqbal/laravel-webpush-kit

Usage

<x-webpush-kit::button
    prompt="auto"
    title="Order updates?"
    body="We'll tell you when your order ships. You can turn this off at any time."
/>

Setup

Install the kit, then set up the channel if you have not already: publish and run its migration, generate VAPID keys, and add its trait to your user model. Set VAPID_SUBJECT to a mailto: address or an https:// URL on a real domain; push:test warns when it is missing or malformed. Push needs HTTPS, except on localhost.

  1. 1 composer require amjadiqbal/laravel-webpush-kit
  2. 2 php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag=migrations
  3. 3 php artisan migrate
  4. 4 php artisan webpush:vapid
  5. 5 Add use HasPushSubscriptions to your User model.
  6. 6 Set VAPID_SUBJECT=mailto:[email protected] in .env.
  7. 7 Put <x-webpush-kit::button /> on a page a signed-in user sees.

Browser support

Version 0.1.0 has been tested in Chrome with Google's push service (FCM) only: subscribing and turning off from the button, normal and declarative pushes delivered and shown, a real 410 removing a dead subscription, Livewire and wire:navigate, and the blocked state. Not tested yet: clicking a delivered notification (the click target is set, but the notification banner never appeared on the test machine), Firefox, and Safari on macOS and iOS. The iPhone and iPad Home Screen guidance is included but untested, so do not rely on it until it has been checked on a device.

The subscribe button

Guests see nothing; the component only renders for a user signed in on the configured guard. Clicking Enable notifications shows a card with your title and body. Allow calls Notification.requestPermission() inside that click and then subscribes with your VAPID key; Not now closes the card without using up the browser's prompt. With prompt="auto" the card appears by itself after delay seconds, only while permission has not been decided, and stays quiet for remind-after-days after Not now. The texts are translatable and the colours are CSS variables such as --wpk-accent, --wpk-text, --wpk-surface and --wpk-border.

<div x-on:webpush-kit:subscribed.window="$wire.$refresh()">
    <x-webpush-kit::button theme="auto" />
</div>

Sending and click targets

Send with the channel as usual. The kit's service worker reads data.url as the page to open on click and data.actions to give each action its own page; for a DeclarativeWebPushMessage it uses navigate and each action's URL. Every push it handles is shown, with the site's host name as the title if the payload has none. Opening the page on click is set up in code but has not been tested on a real delivered notification yet.

return (new WebPushMessage)
    ->title('Your order shipped')
    ->body('Order #1042 is on its way.')
    ->action('Track it', 'track')
    ->data([
        'url' => route('orders.show', 1042),
        'actions' => ['track' => route('orders.track', 1042)],
    ]);

Keeping subscriptions alive

  • Once per browser tab session, a page with the button saves the current subscription again, so a row the server lost comes back.
  • When the browser fires pushsubscriptionchange, the service worker subscribes again if needed and posts the old and new endpoints to the rotate route.
  • The rotate route has no CSRF check, because a service worker cannot read the token. It only accepts same-origin requests from a signed-in user, and only deletes the old endpoint from that user's own subscriptions.
  • If the browser's subscription was made with an old VAPID key, the button unsubscribes it and subscribes again with the current key.

Testing a push

push:test stops if the VAPID keys are missing, warns about a VAPID_SUBJECT that push services may reject, then sends through the channel and prints one row per subscription with the push service host, the HTTP status and the result. A 404 or 410 shows as expired, and the channel deletes that subscription. It exits non-zero when nothing was delivered. With no id it sends to the user who owns the newest subscription. php artisan about shows the kit's version, whether the VAPID keys are set and whether the subject passes the checks.

php artisan push:test                  # owner of the newest subscription
php artisan push:test 42               # user 42
php artisan push:test --url=/orders    # the page a click opens
php artisan push:test --declarative    # a declarative web push message

Already have a service worker?

A scope can only have one service worker. If your app already registers one at /, set service_worker.register to false in config/webpush-kit.php and import the kit's code into yours. To change the kit's own service worker, publish it to public/ and your web server serves that file instead of the route.

self.WEBPUSH_KIT = { rotateUrl: '/webpush/subscriptions/rotate', vapidKey: 'your VAPID public key' };
importScripts('/webpush-sw.js');

// php artisan vendor:publish --tag=webpush-kit-service-worker

iPhone and iPad guidance (untested)

Apple only allows web push for a site added to the Home Screen with a web app manifest whose display is standalone. In iOS Safari outside the Home Screen, the button shows Add to Home Screen steps instead of an error. If your app has no manifest, WEBPUSH_KIT_MANIFEST=true serves one at /manifest.webmanifest from the manifest settings in the config, and <x-webpush-kit::head /> adds the link and the Home Screen meta tags. None of this has been tested on an iPhone or iPad yet.

WEBPUSH_KIT_MANIFEST=true

<head>
    <x-webpush-kit::head />
</head>

Hire me

Need this built into your app, or something custom?

I wrote Laravel WebPush Kit. I can wire it into your Laravel app, extend it for your case, or build the feature around it. Tell me what you need and you'll get a plan and a quote, free.

Support

Found a bug or want a feature? Open an issue on GitHub. For help adding it to your own project, use the quote form above.

More Laravel packages

All packages
Laravel Driver.js: Build Driver.js product tours in PHP and render them with one Blade component.

Laravel Driver.js

Build Driver.js product tours in PHP and render them with one Blade component.

Laravel Sonner: Sonner toast notifications for Laravel, sent from PHP and shown without React.

Laravel Sonner

Sonner toast notifications for Laravel, sent from PHP and shown without React.

Laravel Ninja Keys: A Cmd+K / Ctrl+K command palette for Laravel, with the actions defined in PHP.

Laravel Ninja Keys

A Cmd+K / Ctrl+K command palette for Laravel, with the actions defined in PHP.

Laravel SweetAlert2: SweetAlert2 popups, toasts and confirm dialogs from Laravel controllers, Livewire and Alpine.

Laravel SweetAlert2

SweetAlert2 popups, toasts and confirm dialogs from Laravel controllers, Livewire and Alpine.

Laravel Grid.js: Turn any Eloquent query into a searchable, sortable, paginated Grid.js data table with one fluent PHP call.

Laravel Grid.js

Turn any Eloquent query into a searchable, sortable, paginated Grid.js data table with one fluent PHP call.

Laralink: Work on a Composer package inside the Laravel app that uses it, then switch back to the Packagist release.

Laralink

Work on a Composer package inside the Laravel app that uses it, then switch back to the Packagist release.

Refresher: A drop-in replacement for Laravel's RefreshDatabase that restores a cached snapshot of your test database instead of running migrate:fresh.

Refresher

A drop-in replacement for Laravel's RefreshDatabase that restores a cached snapshot of your test database instead of running migrate:fresh.

Kiln: Artisan commands and an optional signed HTTP route to clear, warm and inspect OPcache when you deploy.

Kiln

Artisan commands and an optional signed HTTP route to clear, warm and inspect OPcache when you deploy.

Laravel Media Viewer: Show private PDFs, video and audio from any Laravel disk without making the files public.

Laravel Media Viewer

Show private PDFs, video and audio from any Laravel disk without making the files public.

Laravel Swiper: Swiper sliders, carousels and image galleries for Laravel as Blade components.

Laravel Swiper

Swiper sliders, carousels and image galleries for Laravel as Blade components.

Laravel MySQL Advisor: Read-only health checks for the MySQL or MariaDB database behind your Laravel app, with the SQL and the Laravel migration to fix a problem wherever a fix applies.

Laravel MySQL Advisor

Read-only health checks for the MySQL or MariaDB database behind your Laravel app, with the SQL and the Laravel migration to fix a problem wherever a fix applies.

Laravel Webhook Console: A dashboard and replay tool for the webhooks spatie/laravel-webhook-client stores in your Laravel app.

Laravel Webhook Console

A dashboard and replay tool for the webhooks spatie/laravel-webhook-client stores in your Laravel app.

Laravel Pennant Dashboard: A browser dashboard for laravel/pennant feature flags: per-scope toggles, sticky percentage rollouts, purge and an audit log.

Laravel Pennant Dashboard

A browser dashboard for laravel/pennant feature flags: per-scope toggles, sticky percentage rollouts, purge and an audit log.

Laravel Notification Inbox: Catches every non-mail Laravel notification in local and staging instead of sending it, and shows each one as it would look on the other end.

Laravel Notification Inbox

Catches every non-mail Laravel notification in local and staging instead of sending it, and shows each one as it would look on the other end.

Laravel Redis Advisor: Read-only health checks for the Redis or Valkey behind your Laravel cache, queues, sessions and Horizon, with the redis.conf line or Laravel config that fixes each problem.

Laravel Redis Advisor

Read-only health checks for the Redis or Valkey behind your Laravel cache, queues, sessions and Horizon, with the redis.conf line or Laravel config that fixes each problem.

Need something built?

Free quote in 60 seconds