emuniq/filament-browser-notifications
Automatically turns every Filament database notification into a browser push notification, no code changes required. Installs VAPID keys, migrations, and the User trait in one command, then auto-registers across all panels with a subscription prompt and profile section.
Before you install, confirm the environment can actually deliver a push. The package has four hard requirements that are easy to overlook in a fresh project: HTTPS (Web Push API refuses to work on plain HTTP outside localhost), a running queue worker, PHP 8.1+ / Laravel 10+ / Filament 3+, and a User model that is writable by the process running artisan.
composer require emuniq/filament-browser-notifications
php artisan browser-notifications:install
That single command does four things you should verify immediately after it finishes:
.env — appends VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT. Confirm VAPID_SUBJECT is a real mailto: or https:// URL; push services use it to contact you about abuse.push_subscriptions table (from laravel-notification-channels/webpush).app/Models/User.php — imports and applies HasPushSubscriptions. Open the file and confirm the trait is in the class body, not just the use statements (this was an actual bug fixed in 1.1.1).Where to look first, in order:
php artisan vendor:publish --tag=browser-notifications-config → config/browser-notifications.php. Read this before you touch any notification code; it governs queue routing and the throttle window./admin/profile in the browser, logged in as yourself. The auto-injected section is the fastest way to confirm the whole stack (service worker route, VAPID keys, subscription persistence) is wired.First use case: take a notification you already send, e.g.
Notification::make()
->title('Invoice paid')
->body('Invoice #1042 was paid by Acme Ltd.')
->sendToDatabase($users);
Enable push in your own profile, then fire the test command to isolate package problems from your own code:
php artisan browser-notifications:test 1 --sync # immediate, bypasses queue
php artisan browser-notifications:test 1 # queued, proves worker is alive
If --sync works but the plain command doesn't, your queue worker is the problem, not the package.
The default pattern is "change nothing." The listener hooks DatabaseNotification::created globally, so every existing sendToDatabase() call — and every $user->notify(new SomeNotification) that writes a database channel row — starts producing pushes. The most common integration task in a mature codebase isn't adding code, it's auditing which existing notifications now hit users' operating systems.
Selective suppression. Not every database notification deserves an OS-level interruption. Mark the low-value ones:
Notification::make()
->title('Weekly digest ready')
->viewData(['silent' => true]) // note: viewData, not data()
->sendToDatabase($users);
A sensible convention in a real app: silently write to the bell icon for anything the user can catch up on, and let only genuinely time-sensitive events (approval requests, payment failures, assignment) reach the browser.
Tuning the throttle for your domain. The 5-second default groups bursts into a single "You have N new notifications" push. That is right for fan-out events (a bulk import finishing, a mass assignment), wrong for a transactional app where each notification carries distinct meaning. Drop it to 0 to disable grouping if individual titles matter more than volume — but then audit any loop that notifies the same user repeatedly, because you'll get one OS popup per iteration.
Queue isolation. Route push dispatch to its own queue so a slow push endpoint never delays your mail or exports:
'queue_connection' => 'redis',
'queue_name' => 'notifications',
Panel provider override. The plugin auto-registers, but adding it explicitly in PanelProvider::panel()->plugins([...]) overrides the auto-registration — that's the supported way to change behaviour, and it's how you scope the feature per panel (e.g. prompt after 2s in the admin panel, disabled entirely on a customer-facing portal by simply not adding it there).
BrowserNotificationsPlugin::make()
->promptDelay(5)
->dismissCooldownDays(14)
->profileSection(false), // when you build your own preferences UI
Bring-your-own UI. profileSection(false) plus the HasPushSubscriptions trait gives you $user->pushSubscriptions to count devices yourself, if you want push preferences to live inside a broader "Notification settings" resource rather than the profile page.
iOS is a different onboarding flow, not a bug. Treat "add to home screen" as a required step in your user docs for iOS users; the package detects Safari-without-PWA and shows instructions in the profile section, but it cannot bypass the platform requirement.
DatabaseNotification model, not Filament. Any notification that persists to the notifications table triggers a push — including ones written by packages or by plain Laravel Notification classes with custom toDatabase() payloads. Test your non-Filament notifications separately; title/body/URL extraction is tuned for Filament's data shape and custom payloads may yield a generic push.User notifiables aren't covered. The installer only patches User. If you notify Team, Admin, or a multi-guard model, add HasPushSubscriptions there by hand and expect the profile section not to appear for them.use HasPushSubscriptions, Notifiable; — the 1.1.1 fix handles use Notifiable, HasRoles; style declarations, but if you're on an older tag and hit BadMethodCallException on pushSubscriptions(), that's the cause./sw.js or similar) and that the page is served over HTTPS. Browser devtools → Application → Service Workers is where you'll actually see the failure; server logs won't show anything.dismissCooldownDays is enforced via localStorage, so it resets when a user clears storage or switches device. Don't treat it as a security or compliance control.ActionGroup. Nested actions were only resolved as of 1.2.0, and pre-1.2 grouped pushes fell back to a hardcoded /admin. If you run multiple panels or a non-/admin path, be on ≥1.2.0 or clicks will land on the wrong page.manifest.json inheritance. start_url and theme_color derive from the active panel, which is usually right — but theme_color conversion is per-Filament-major (RGB triplets in v3, oklch() in v4/v5). Verify the installed PWA chrome after a Filament upgrade; pin overrides in config if you need stability.--sync test → queue worker → push_subscriptions row exists for the user → service worker registered → VAPID keys match the environment. Most "it doesn't work" reports resolve at step 3 or 5, especially after copying a database between environments without copying .env./admin, trait patching, silent in v5). Pin a version, read the changelog before upgrading, and keep the surface small: you're adopting a listener on a core Eloquent event, so a regression has app-wide blast radius. The escape hatch is good — it's an event listener and a queued job, both replaceable without touching your notification classes.How can I help you explore Laravel packages today?