xcoorp/laravel-webpush-notifications
A Laravel package for sending Web Push notifications to browsers. It integrates with Laravel’s notification system, manages push subscriptions and VAPID keys, and lets you deliver messages even when users aren’t actively on your site.
This package gives you browser Web Push (the notifications that appear via the service worker, even when the tab is closed) backed by a push_subscriptions table and a Laravel notification channel. It is not FCM/APNs — there is no Google/Apple account involved; you own the VAPID key pair.
First steps:
composer require xcoorp/laravel-webpush-notifications — note that v1.0.3 bumps the dependency constraints to allow Laravel 13. Nothing in the public API changed in this release; the changelog is entirely dependency bumps and lockfile maintenance, so an upgrade from v1.0.2 is a composer update with no code changes. If you are pinned below that, you can still upgrade confidently.php artisan vendor:publish --tag="webpush-migrations" then php artisan migrate. The table stores an endpoint, a public_key/auth_token pair, and a morph to your user model.php artisan vendor:publish --tag="webpush-config" and php artisan webpush:vapid (or generate externally and set WEBPUSH_VAPID_PUBLIC_KEY, WEBPUSH_VAPID_PRIVATE_KEY, WEBPUSH_VAPID_SUBJECT). Commit the public key to your frontend build; keep the private key in the environment only, and never in a committed .env.example with a real value.User model — typically HasPushSubscriptions, exposing a pushSubscriptions() morph-many relation.Your first useful use case is the simplest one: on page load, register the service worker, call PushManager.subscribe() with the VAPID public key, and POST the resulting subscription JSON to an endpoint that does $request->user()->updatePushSubscription(...). Then send a Notification with the web push channel and watch it land. Get that loop working for one hardcoded user before wiring it into real product events.
Notification channel is the idiomatic path. Define a notification class, add the package's channel to via(), and implement the message builder to return a WebPushMessage (title, body, icon, badge, action buttons, deep-link URL). This keeps web push sitting alongside mail and database in the same class, which is what you want for anything user-facing — one notification, several transports, consistent preferences handling.
Store subscriptions per device, not per user. The morph relation naturally allows many rows per user. Treat each row as a device/browser install and delete on 410 Gone / 404 responses, which the package surfaces. Plan a cleanup job from day one: push endpoints expire constantly, and a table full of dead endpoints slows every broadcast.
Fan-out patterns. For single-user sends, $user->notify(new ...) is fine. For anything bulk (a campaign, a “your report is ready” blast to a segment), use Notification::send() with a chunked cursor query and queue the notification. Web push is fast per-request but you are making one outbound HTTPS call per subscription — 50k subscribers in a request cycle will time out.
Trigger from domain events, not controllers. The pattern that ages best: a listener on OrderShipped or ReportReady dispatches the notification, and the notification decides its own channels. You then add email later without touching the event.
Service worker placement. The service worker file must be served from the site root scope you want to control. Keep the push and notificationclick handlers minimal — display the payload, and on click, focus an existing client or open the deep link. Business logic belongs in the payload, not in the SW.
Local development. Push requires HTTPS (or localhost). Test against a real browser with a real key pair; the Symfony/web-push failure modes are opaque and you will waste an hour if you stub the transport and assume it works.
notificationclick handler pinging your own endpoint if you need real engagement data.Notification.requestPermission() on page load. Browsers penalise this, and once a user blocks, you cannot re-prompt programmatically — only they can undo it in site settings. Ask behind a user gesture and an explained benefit.ShouldQueue. Set a sensible timeout so one dead endpoint does not stall a worker.WEBPUSH_* values read at boot via config() are baked into the cached config. If you rotate keys in production, php artisan config:cache must be re-run.notify() behind a NotificationPreference service) rather than forking the package — nothing in the v1.0.3 release changes those seams, so customisations against v1.0.2 remain valid.^1.0 and let dependency bumps flow in via your normal scheduled composer update. There is no migration or config diff to chase here.How can I help you explore Laravel packages today?