Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Notification Bell Laravel Package

caiquebispo/notification-bell

An elegant, framework-agnostic Laravel notification bell for Livewire with per-user preferences, sound and category controls, dark mode, and a zero-build delivery pipeline. No Tailwind, Bootstrap, or npm needed—install via Composer, drop the component in your layout, done.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and see it work in under five minutes.

composer require caiquebispo/notification-bell
php artisan migrate      # package migrations are auto-loaded; no publish needed

Add the trait to your User model, drop the component into your layout, and make sure @livewireScripts is present exactly once:

use CaiqueBispo\NotificationBell\Traits\HasNotifications;

class User extends Authenticatable
{
    use HasNotifications;
}
@auth
    <livewire:notification-bell />
@endauth

@livewireScripts

Then start a worker, because notifications are queued by default — this is the single most common "it doesn't show up" moment:

php artisan queue:work

Smoke-test from Tinker before touching your domain code:

NotificationHelper::info(1, 'Hello', 'First bell notification');

If nothing renders, check in this order: (1) queue worker running, (2) @livewireScripts present, (3) the user you notified is the authenticated user, (4) the injected stylesheet route isn't blocked by middleware or CSP.

Where to look first. Publish only the config — it's the package's real documentation:

php artisan vendor:publish --tag="notification-bell-config"

Read config/notifications.php end to end (theme, types, features, delivery guards, broadcasting, api, route) and skim Helpers/NotificationHelper.php in vendor/. Everything else — views, lang, migrations — you should leave unpublished until you actually need to override something.

First real use case. The natural day-one win is transactional notification: order shipped, invoice paid, document awaiting approval. You get a bell, a badge, toasts, sounds, per-user preferences, a history page and a full REST API without writing a single Blade file or a Tailwind build.

Implementation Patterns

Drive notifications from domain events, not controllers. This keeps the package at arm's length and makes dedup trivial:

Event::listen(OrderShipped::class, function (OrderShipped $event) {
    NotificationHelper::success(
        $event->order->user_id,
        'Order shipped',
        "Order #{$event->order->id} is on its way",
        ['order_id' => $event->order->id],
        route('orders.show', $event->order->id),
        [
            'category'   => 'orders',
            'dedup_key'  => "order-shipped-{$event->order->id}",
            'group_key'  => "order-{$event->order->id}",
        ]
    );
});

The dedup_key derived from a domain identity is the highest-value pattern in the whole package: queue retries, webhook replays and double-fired events collapse automatically inside the dedup window.

Separate your taxonomy: type is visual, category is a user contract. types control colour and icon and belong to you. categories are what users mute — treat them as a public API you promise not to rename casually, and register them in config so mute controls render properly.

Group aggressively, but know what you're hiding. group_key collapses N notifications into one expandable row. Ideal for comment threads and per-order activity. It is not ideal when each item carries its own action buttons, because users only see the collapsed summary until they expand.

Use scheduling and expiry for time-bound work. Reminders get scheduled_at; promos and maintenance banners get expires_at. This lets you write the notification at the moment of business logic rather than choreographing a later job.

Treat user preferences as part of your delivery decision, not an afterthought. A notification to a muted category is silently skipped — so if you also send email, read the preference yourself:

$prefs = $user->bellPreferences();
if (! $prefs->isSnoozed()) {
    Mail::to($user)->queue(new OrderShippedMail($order));
}

Headless usage for SPAs and mobile. Disable the Livewire UI entirely if you have your own frontend. Enable api.enabled with auth:sanctum, then consume /api/notifications, /unread-count, /stats, and the mutation endpoints. All endpoints are scoped to the authenticated user, so you never pass a user id from the client. Pair this with broadcasting and a channel auth entry:

Broadcast::channel('notifications.{userId}', fn ($user, $userId) => (int) $user->id === (int) $userId);

Wrap the helper behind your own service. One class in your app (App\Support\Notifier) that takes a DTO and calls NotificationHelper::create(). Two benefits: you get a seam to swap packages later, and you get a single place to enforce your category list and default queue setting.

Testing pattern. In tests, freeze time (so scheduled_at/expires_at behave), pass 'queue' => false, and assert on the database plus the package events rather than on rendered HTML:

Event::fake([NotificationCreated::class]);
NotificationHelper::info($user->id, 'T', 'M', [], null, ['queue' => false]);
Event::assertDispatched(NotificationCreated::class);

Because mutes, rate limits and dedup all drop silently, write explicit tests for those three behaviours — they're the ones that will surprise you in production.

Mirror to other channels via events. Listen to NotificationCreated and forward to Slack/e-mail if the channel matters; listen to NotificationRead/NotificationArchived for analytics. This keeps the package as the in-app store rather than the source of truth for delivery.

Gotchas and Tips

Queued by default. Notifications do not exist until a worker processes the job. On shared hosting without a worker, set 'queue' => false per call or change the default — otherwise the bell is permanently empty and looks broken.

Three silent-drop mechanisms. Deduplication (default 300s window), rate limiting (30/min per user), and muted categories. None throw, none log loudly. If you bulk-fan-out inside a loop to the same user, or retry a job after five minutes, you'll lose notifications without a trace. Raise rate_limit.max_per_minute if you legitimately spike, and set dedup_key deliberately rather than accidentally.

The README's action-button example is suspect. It passes the actions array as the fourth positional argument of info(), which is documented as data. Use the options array form (create(..., ['actions' => [...]])) and verify against NotificationHelper's actual signature in vendor/ before wiring it.

Action buttons are GET navigations. ['label' => 'Approve', 'url' => '/docs/1/approve'] will fire a state change over GET with no CSRF token. Either point actions at confirmation pages, or use signed URLs with a short expiry. Don't let a notification click mutate data directly.

theme.mode => 'auto' deliberately ignores the OS. It only follows explicit .dark / [data-theme="dark"] / [data-bs-theme="dark"] markers. If your app has no dark-mode marker but you expect OS-following, set 'system' and be aware a light site will then go dark for dark-OS users.

Route collisions and admin exposure. The admin panel defaults to /notifications while history lives at /notifications/history. On an app that already has a /notifications route, or with a permissive prefix, you can shadow your own pages. Set route.prefix => 'admin/notifications' and a real middleware stack (auth, role:admin or a gate) — the config default will otherwise expose full create/send-to-all-users powers. Consider 'features' => ['history_page' => false] if you already have a notification centre.

Assets come from a vendor route. Styles are served through a package route rather than @vite. That means: don't strip vendor/ in deploy, make sure the route isn't behind a middleware that redirects, and check your CSP — inline CSS variables plus an injected <style> frequently need style-src 'unsafe-inline'. If you need the stylesheet in the head, use @notificationBellStyles and drop the auto-injection expectation.

One bell per page, and disable polling when Echo is live. Each <livewire:notification-bell /> is its own polling component. Multiple instances (e.g. mobile + desktop nav both rendered) multiply requests. Use :polling="false" on secondary instances and whenever websockets are configured — fallback_to_polling handles the graceful case but won't know about your duplicate bells.

Cleanup is destructive-by-design; dry-run first.

php artisan notifications:cleanup --dry-run
php artisan notifications:cleanup --days=60 --unread-days=120

It purges soft-deleted trash for real, so the "undo delete" window is bounded by your schedule. Check keep_pinned / keep_archived before scheduling it daily, and confirm the interaction between cleanup and not-yet-delivered scheduled_at rows in your version.

Localization coverage is two languages. en and pt_BR. Every other locale silently falls back to English, including relative dates. For multi-locale apps, publish the lang tag and add files, or set locale per instance. Also note the per-instance locale="pt_BR" override — useful when the UI language should differ from app()->getLocale().

Sound will not autoplay. Browsers block Web Audio until the user interacts with the page. Never make sound the only signal for something important; toasts and the badge are the reliable channels.

Scoping is per-user-id only. There's no polymorphic notifiable, no team/tenant column. For org-wide or role-based notifications, fan out to the member ids yourself (or use notifications:send-bulk --users=...), and accept that this scales linearly.

Pin the version and read the source. Two stars and zero dependents means you are the QA team. Pin an exact version in composer.json, skim migrations and the helper before upgrading, and write a handful of contract tests around the API endpoints and helper signatures you depend on. The redeeming qualities — idempotent, reconciling migrations and a schema-readiness guard — suggest the author has thought about production deploys, but that's a claim to verify on a staging deploy, not to trust.

Best extension points, in order: theme.bell_icon and theme.item_view (custom Blade per row, no forking) → publishing views → publishing lang → events for cross-channel mirroring. Config theming via CSS variables means you can usually get 90% of a design match without touching a single package file.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route