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

Nova Notifications Laravel Package

mirovit/nova-notifications

mirovit/nova-notifications integrates Laravel notifications with Nova, adding a notification bell or inbox to your admin panel so you can send, display, and manage user notifications. It provides a clean, native Nova experience with simple setup for Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish

composer require mirovit/nova-notifications
php artisan vendor:publish --tag=nova-notifications   # if shipped; check the provider's publishes() calls

This is a Nova tool, not a resource, so it does not appear in the sidebar by default — it renders as a bell in the Nova header. There is almost certainly nothing to migrate: it reads Laravel's stock notifications table, so it is a no-op if you already use database notifications.

2. Satisfy the backend prerequisites

php artisan notifications:table
php artisan migrate

Confirm the model you authenticate against uses the trait and that Nova's guard resolves to it:

use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;
}

If Nova uses a different model (e.g. Admin), the tool will query notifications for that model — so whichever class is behind config('nova.guard') is the one that must be Notifiable.

3. Register the tool

// app/Nova/NovaServiceProvider.php
use Mirovit\NovaNotifications\NovaNotifications;

public function tools()
{
    return [
        new NovaNotifications(),
    ];
}

4. Where to look first in the source

The package is small; read it in this order before you write any application code:

  1. src/NovaNotifications.php — the Laravel\Nova\Tool subclass. This is where boot(), route registration, and any menu/asset wiring lives. If the bell does not appear, the answer is here.
  2. routes/ or the provider's boot() — the tool's HTTP endpoint. Note its middleware group and how it resolves the current user.
  3. src/Http/Controllers/* — the list / mark-as-read actions. This is where the payload-to-Vue contract is defined.
  4. resources/js/ + dist/ — the Vue component that renders the dropdown. Read the .vue file to learn the exact payload keys it expects; dist/ is prebuilt and you should not need to touch it.
  5. config/ — polling interval, page size, and display toggles, if any.

5. First use case: get one notification into the bell

Write a minimal notification that writes a database row with the payload shape the Vue component expects (title, message/body, level, optional url):

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;

class OrderFailed extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(public array $payload) {}

    public function via($notifiable): array
    {
        return ['database'];
    }

    public function toArray($notifiable): array
    {
        return [
            'title'   => 'Order failed',
            'message' => "Order #{$this->payload['id']} could not be charged.",
            'level'   => 'error',                       // confirm allowed values in the Vue component
            'url'     => "/resources/orders/{$this->payload['id']}",
        ];
    }
}

Seed one and reload Nova:

php artisan tinker
>>> App\Models\User::first()->notify(new App\Notifications\OrderFailed(['id' => 1234]));

If the bell shows nothing, the problem is one of three things — and this is the fastest triage order: via() does not include 'database', the notification was queued and the worker is not running, or the row exists with notifiable_type that does not match your morph alias (see Gotchas).

Implementation Patterns

Adopt a payload DTO so every notification renders. The single highest-value thing you can do is stop writing ad-hoc arrays. Define one shape and enforce it:

final class InboxPayload
{
    public function __construct(
        public string $title,
        public string $message,
        public string $level = 'info',
        public ?string $url = null,
    ) {}

    public function toArray(): array
    {
        return [
            'title'   => $this->title,
            'message' => $this->message,
            'level'   => $this->level,
            'url'     => $this->url,
        ];
    }
}

Then a thin base class keeps every notification consistent and gives you one place to adapt when you eventually replace the package:

abstract class InboxNotification extends Notification implements ShouldQueue
{
    public function via($notifiable): array { return ['database']; }

    abstract public function payload($notifiable): InboxPayload;

    public function toArray($notifiable): array
    {
        return $this->payload($notifiable)->toArray();
    }
}

Wrap, don't rewrite, existing notifications. You almost certainly already have notification classes firing that store free-form data. Do not touch their mail/Slack channels — add 'database' to via() and normalise the payload only in toArray(). That keeps existing behaviour intact while opting notifications into the inbox one at a time.

Fan out with Notification::send and an explicit audience. The package is a display layer; routing logic belongs in your app.

Notification::send(
    User::role('ops')->get(),
    new OrderFailed($order->only('id'))
);

For large audiences, prefer the facade over a loop — it batches and queues properly. For role-based fan-out where the audience is large, consider a single ShouldQueue listener that dispatches individual notifications so a failure in one does not drop the batch.

Trigger from observers/events, not controllers. Keeps the notification source of truth next to the domain event:

class OrderObserver
{
    public function updated(Order $order): void
    {
        if ($order->wasChanged('status') && $order->status === 'failed') {
            Notification::send($order->account->admins, new OrderFailed($order->only('id')));
        }
    }
}

Read/unread: mark the notification read on navigation. The package will have a mark-as-read endpoint. The clean end-to-end pattern is a notification whose url points at the tool's mark-as-read route with a redirect back, or a small Nova custom field/action that POSTs to it. Either way, mark-as-read should happen on click, not on list render — otherwise unread badges are meaningless the moment the dropdown opens.

Keep Nova's toasts and this inbox semantically separate. Nova::notify() is a session flash — it is gone on refresh. This inbox is persistent, per-user, database-backed. Pick a convention and document it: toasts for "your action succeeded", inbox for "something happened that you need to see later". Without that line, admins get two bells of information and trust neither.

Multi-tenancy: scope at the query layer, not the UI. If you run per-tenant databases, the tool's controller reads the notifications table on the currently resolved connection. Verify it uses Nova::user() (or the Nova guard) rather than Auth::user(); if it does not, a fork patch is the fix — this is the one place you should not accept "it looks like it works".

Test the contract, not the UI. Use Notification::fake() to assert who got notified, and a feature test against the tool's route to assert the payload renders:

Notification::fake();

$this->actingAs($admin, 'web');
Order::factory()->failed()->create();

Notification::assertSentTo($admin, OrderFailed::class);

Then a second test hitting the tool endpoint, asserting a 200, a per-user row count, and that user B's notifications are not in user A's response. That last assertion is the regression test that matters after every Nova upgrade.

Gotchas and Tips

Treat this as a fork candidate, not a dependency. Last release was March 2022, zero dependents, single maintainer. It targets the Nova 4 era; Nova 5's move toward Inertia/Vue 3 for tool front-ends is a plausible break, and PHP 8.3/8.4 + Laravel 11/12 drift is near-certain. Vendor the source into packages/ with a Composer path repository and pin laravel/nova, php, and laravel/framework explicitly. Budget one to two engineer-days per Nova major upgrade with no upstream to lean on.

Nova is a paid, private package. CI cannot resolve laravel/nova without a license credential. Either inject it as a CI secret or mock the Nova user resolver and test the controller directly.

Morph map mismatch is the #1 "silent emptiness" bug. If your app registers a morph map in Relation::morphMap(), notifiable_type stores the alias ('user'), not the FQCN. If the tool queries by class name, the bell renders and shows nothing, with no error. Check the actual rows first:

DB::table('notifications')->select('notifiable_type')->distinct()->get();

Out-of-contract payloads blank the dropdown. Because existing notification classes store free-form arrays, any row whose data lacks the expected keys can throw in the Vue component — and a single bad row can take out the whole list. Mitigations: normalise everything through the DTO above, and patch the controller to filter or default missing keys before serialising. A ?? '' in the backend is cheaper than a white-screen dropdown in production.

On-demand/anonymous notifications never appear. Notification::route('mail', ...) writes rows with a null notifiable_id and a string notifiable_type. They are not addressed to a user and will not — and should not — show up. If you are debugging "missing notification", check this before touching code.

Authorization is a release blocker. Confirm every tool route sits inside Nova's middleware group and scopes to Nova::user(). A route that falls back to the default guard, or that accepts an arbitrary notification ID on mark-as-read without an ownership check, is a cross-user notification leak. Test it explicitly: authenticate as user B and attempt to mark user A's notification read.

Deleted notifiables produce orphans. When a user is deleted, their notification rows survive. Mark-as-read on an orphan, or a notifiable relation that no longer resolves, can 500 the endpoint. Either cascade-delete in a model observer or make the controller tolerant of a missing notifiable.

Plan for table growth and add the right index. notifications grows monotonically. Add a composite index supporting the read path and a pruning job:

// migration
$table->index(['notifiable_type', 'notifiable_id', 'read_at']);

// app/Console/Kernel.php
$schedule->command('notifications:prune', ['--hours' => 720])->daily();

Watch for N+1s if the controller eager-loads per-notification relations, and confirm the endpoint paginates — if it loads every unread row on each dropdown poll, concurrent admins generate cheap but real query load.

Rebuild the front-end, don't trust dist/. Nova tools ship prebuilt assets. Before you commit to a fork, verify npm run build (or the Nova Mix/Vite scaffolding in the package) actually compiles under your Node version. If it does not, your fork is frozen at whatever the checked-in bundle does — which is a hard ceiling on any customisation.

Leave yourself a kill switch. Register the tool behind a config flag so a Nova upgrade that breaks it does not block an unrelated admin-panel deploy:

public function tools()
{
    return array_filter([
        config('nova.notifications_inbox') ? new NovaNotifications() : null,
    ]);
}

Debugging checklist, in order: row exists in notifications? → notifiable_type matches your morph map? → via() includes 'database'? → queue worker running? → payload has the keys the Vue component reads? → route reachable under the Nova guard? Most of these take under a minute each once the tool is in place, and they cover essentially every failure mode this package has.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
cgv/localized-resource