Search by

jeremykenedy / laravel-notifications

jeremykenedy

A Laravel package providing an in-app notification center with bell/badge UI, read/unread tracking, and REST API.

Package info

github.com/jeremykenedy/laravel-notifications

pkg:composer/jeremykenedy/laravel-notifications

Statistics

Installs: 14

Dependents: 0

Suggesters: 0

Stars: 5

Open Issues: 0

v2.0.1 2026-09-11 14:55 UTC

README

Laravel Notifications

Laravel Notifications

A complete in-app notification center for Laravel with bell badge, unread count, mark as read, archive, delete, real-time WebSocket broadcasting via Reverb, and a full REST API. Ships Blade views for Tailwind CSS, Bootstrap 5 and Bootstrap 4, plus a Livewire component and Inertia starter components for Vue, React and Svelte.

Total Downloads Latest Stable Version Tests StyleCI License: MIT

Table of Contents

Features

  • Notification center page with inbox and archive views
  • Bell icon with live unread count badge
  • Mark as read, mark as unread, mark all as read
  • Archive, restore and archive all
  • Delete single and delete all, with a confirmation modal
  • Client side filtering of the visible list
  • Real-time updates over WebSockets via Laravel Reverb
  • Full JSON API covering every action the web UI offers
  • A send notification GUI for broadcasting to all users or a single role
  • Three CSS frameworks and five frontends, selected by config
  • Every user facing string translatable

Requirements

  • PHP 8.2 or newer
  • Laravel 10, 11, 12 or 13
  • A notifications database table, created by Laravel's own notifications migration

Continuous integration runs against PHP 8.2, 8.3 and 8.4 on Laravel 12 and Laravel 13. Laravel 10 and 11 remain within the Composer constraint but are not currently exercised in CI, because every published release on those branches is blocked by an upstream security advisory and cannot be installed. Laravel 13 requires PHP 8.3 or newer.

Installation

composer require jeremykenedy/laravel-notifications
php artisan notifications:install --css=tailwind --frontend=blade
php artisan migrate

The package needs Laravel's notifications table. If your application does not have one, the package migration creates it, including the archived_at column it adds. If you would rather own that migration yourself, generate it before installing the package:

php artisan make:notifications-table
php artisan migrate

The views extend layouts.app and push page styles onto a template_linked_css stack. Publish the views if your layout differs:

php artisan vendor:publish --tag=notifications-views

Configuration

php artisan vendor:publish --tag=notifications-config
Key Default Description
css_framework null tailwind, bootstrap5 or bootstrap4. Falls back to ui-kit.css_framework, then tailwind.
frontend null blade, livewire, vue, react or svelte. Falls back to ui-kit.frontend, then blade.
per_page 20 Notifications per page.
bell.show_count true Render the count badge on the bell.
bell.max_count_display 99 Counts above this render as 99+.
bell.poll_interval_ms 30000 How often the bell polls for the count.
routes.enabled true Master switch for every route the package registers.
routes.prefix notifications URI prefix for the web routes.
routes.middleware ['web', 'auth'] Middleware for the web routes.
api.enabled true Register the JSON endpoints. Requires routes.enabled.
api.prefix api/notifications URI prefix for the JSON endpoints.
api.middleware ['api', 'auth:sanctum'] Middleware for the JSON endpoints.
flash_messages false Show inline success alerts. Leave off if you use a toast package.
send.enabled true Register the send notification GUI.
send.middleware ['web', 'auth', 'verified', 'level:5'] Middleware for the send GUI.
user_model App\Models\User Model used by sendToAll and sendToRole.
role_model App\Models\Role Model used by the send GUI role picker.
broadcast.enabled true Broadcast a NotificationCreated event on every database notification.

Sanctum is not a dependency of this package. If your API is guarded some other way, change api.middleware.

Usage

Notification Center Page

Navigate to /notifications. The page shows:

  • Unread notifications highlighted, with a coloured icon per notification type
  • Title, message and a relative timestamp
  • An action link when the notification carries a URL
  • Inbox and Archived tabs, with a count on the archive
  • A filter box that narrows the visible list without a round trip
  • Per notification actions: mark read, mark unread, archive, restore, delete
  • Bulk actions: mark all read, archive all, delete all

Bell Badge with Unread Count

@include('notifications::partials.bell')

The partial renders in whichever CSS framework is active. It fetches the count from GET /notifications/count on load, polls on bell.poll_interval_ms, and updates instantly over WebSockets when Reverb is running. The badge hides at zero and shows 99+ past bell.max_count_display.

Bootstrap 5 markup uses Bootstrap Icons and Bootstrap 4 markup uses Font Awesome. Include whichever icon font your layout already loads.

Sending Notifications

Any notification sent on the database channel shows up in the notification center:

$user->notify(new OrderShipped($order));

The package ships AppNotification for the common case:

use Jeremykenedy\LaravelNotifications\Services\NotificationService;

app(NotificationService::class)->send(
    users: $user,
    title: 'Invoice ready',
    message: 'Invoice 1284 has been generated.',
    type: 'success',
    actionUrl: '/invoices/1284',
    actionText: 'View invoice',
);

Types are info, success, warning, danger and system. Each renders a different icon colour.

Mark as Read

$service = app(NotificationService::class);

$service->markAsRead($user, $notificationId);
$service->markAsUnread($user, $notificationId);
$service->markAllAsRead($user);

Archive Notifications

Archiving keeps a notification without leaving it in the inbox. Archiving an unread notification also marks it read, one at a time or in bulk. Notifications that were already read keep the time they were originally read.

$service->archive($user, $notificationId);
$service->unarchive($user, $notificationId);
$service->archiveAll($user);

Delete Notifications

$service->delete($user, $notificationId);
$service->deleteAll($user);

CSS Framework Support

The notification center renders with the framework named by config('notifications.css_framework'). When that is empty the package falls back to config('ui-kit.css_framework'), then to tailwind, so an application already driven by UI_KIT_CSS keeps the framework it is on.

Framework Views Notes
Tailwind CSS resources/views/tailwind/blade Tailwind utilities with a dark: variant on every colour. Alpine for the filter and modals.
Bootstrap 5 resources/views/bootstrap5/blade Bootstrap 5 utilities and Bootstrap Icons.
Bootstrap 4 resources/views/bootstrap4/blade Bootstrap 4 utilities and Font Awesome.

Each framework ships its own index, send and partials/bell views. No framework's classes appear in another framework's views.

Frontend Framework Support

Frontend What ships Status
Blade Full notification center, send GUI and bell for all three CSS frameworks Complete, covered by the test suite
Livewire NotificationsList component with mark read, mark all read and delete Tailwind markup only
Vue, React, Svelte NotificationsIndex Inertia starter components Starting points you wire into your own Inertia page

Livewire

<livewire:notifications-list />

Methods: markAsRead($id), markAllAsRead(), delete($id). Paginates on per_page and lists the inbox, so archived notifications are excluded. The component delegates to NotificationService, so it obeys the same ownership checks as everything else.

Vue, React and Svelte

php artisan notifications:install --frontend=vue

Publishes NotificationsIndex.vue to resources/js/Pages/Notifications/. React and Svelte publish the equivalent file.

These are starting points, not a finished integration. The package's own controllers return Blade views, so rendering one of these components means adding an Inertia route of your own that hands it a paginator. They expect a created_at_human field on each notification, which your controller supplies.

Changing Frameworks

Two commands change which views render. Both write to .env and clear a cached config.

# Change the CSS framework
php artisan notifications:switch --css=bootstrap5

# Change the frontend
php artisan notifications:switch --frontend=vue

# Change both at once
php artisan notifications:switch --css=tailwind --frontend=livewire
Option Values
--css tailwind, bootstrap5, bootstrap4
--frontend blade, livewire, vue, react, svelte

notifications:install takes the same options and additionally publishes the config file. With neither option given it prompts for both.

Switching writes NOTIFICATIONS_CSS_FRAMEWORK and NOTIFICATIONS_FRONTEND. If you have published the views, your published copies keep rendering until you publish again.

Real-Time Broadcasting

With Reverb configured, every database notification dispatches NotificationCreated on a private channel:

window.Echo.private(`notifications.${userId}`)
    .listen('.notification.created', (event) => {
        console.log(event.count);
    });

The channel is notifications.{userId} and the event name is notification.created. The payload carries userId, title, message and the recipient's current unread count. User keys may be integers, UUIDs or ULIDs.

Authorize the channel in routes/channels.php:

Broadcast::channel('notifications.{userId}', function ($user, $userId) {
    return (string) $user->getKey() === (string) $userId;
});

Turn it off with broadcast.enabled. A broadcast failure is reported to your exception handler and never fails the notification itself.

Web Routes

Method URI Name Description
GET /notifications notifications.index Notification center. ?archived=1 for the archive
GET /notifications/count notifications.count Unread count as {"count": 5}
POST /notifications/{id}/read notifications.read Mark as read
POST /notifications/{id}/unread notifications.unread Mark as unread
POST /notifications/read-all notifications.read-all Mark all as read
POST /notifications/{id}/archive notifications.archive Archive
POST /notifications/{id}/unarchive notifications.unarchive Restore from archive
POST /notifications/archive-all notifications.archive-all Archive all
DELETE /notifications/{id} notifications.destroy Delete
DELETE /notifications notifications.destroy-all Delete all
GET /notifications/send notifications.send.create Send notification GUI
POST /notifications/send notifications.send.store Send a notification

API Routes

Method URI Name Description
GET /api/notifications api.notifications.index Paginated notifications. ?per_page=
GET /api/notifications/unread api.notifications.unread Unread notifications
GET /api/notifications/count api.notifications.count Unread count
POST /api/notifications/{id}/read api.notifications.read Mark as read
POST /api/notifications/{id}/unread api.notifications.mark-unread Mark as unread
POST /api/notifications/read-all api.notifications.read-all Mark all as read
POST /api/notifications/{id}/archive api.notifications.archive Archive
POST /api/notifications/{id}/unarchive api.notifications.unarchive Restore from archive
POST /api/notifications/archive-all api.notifications.archive-all Archive all
DELETE /api/notifications/{id} api.notifications.destroy Delete
DELETE /api/notifications api.notifications.destroy-all Delete all

Every endpoint is scoped to the authenticated user. Acting on a notification that is missing or belongs to someone else changes nothing and still answers 200.

NotificationService API

use Jeremykenedy\LaravelNotifications\Services\NotificationService;

$service = app(NotificationService::class);

$service->unreadCount($user);                 // int
$service->archivedCount($user);               // int
$service->getAll($user, $perPage);            // paginator, inbox and archive
$service->getActive($user, $perPage);         // paginator, inbox only
$service->getArchived($user, $perPage);       // paginator, archive only
$service->getUnread($user, $perPage);         // paginator, unread and not archived

$service->markAsRead($user, $id);             // bool
$service->markAsUnread($user, $id);           // bool
$service->markAllAsRead($user);               // int, how many changed
$service->archive($user, $id);                // bool
$service->unarchive($user, $id);              // bool
$service->archiveAll($user);                  // int, how many changed
$service->delete($user, $id);                 // bool
$service->deleteAll($user);                   // int, how many deleted

$service->send($users, $title, $message, $type, $actionUrl, $actionText, $sendEmail, $icon);
$service->sendToAll($title, $message, $type, $actionUrl, $actionText, $sendEmail);   // int
$service->sendToRole($slug, $title, $message, $type, $actionUrl, $actionText, $sendEmail); // int

The single notification methods return false when the id does not exist or belongs to another user, and never touch a row they do not own.

Artisan Commands

Command Options Description
notifications:install --css, --frontend Publish the config and set both frameworks. Prompts when an option is omitted.
notifications:switch --css, --frontend Change either framework, or both. At least one is required.
Publish tag Publishes to
notifications-config config/notifications.php
notifications-views resources/views/vendor/notifications
notifications-lang lang/vendor/notifications
notifications-vue, notifications-react, notifications-svelte resources/js/Pages/Notifications

Translations

Every string in every view comes from notifications::notifications. Publish the file to translate or reword:

php artisan vendor:publish --tag=notifications-lang

Testing

composer test

The suite runs on Orchestra Testbench against an in memory SQLite database and refuses to start against anything else. It covers:

  • The service layer, including ownership checks and the archive and unread paths
  • Every web route and every JSON endpoint, authenticated and as a guest
  • The notification center, archive view, empty state and send form rendered in all three CSS frameworks
  • The send GUI, including validation and sending to a role
  • All fifteen install and all fifteen switch framework combinations
  • The migration, including a missing notifications table and being run twice
  • Broadcasting, including integer and UUID user keys
  • Config defaults, framework resolution and translation coverage
composer lint        # apply Pint
composer lint:test   # check style without writing

Upgrading

Nothing in the public API was renamed or removed, and no route name changed. Three behaviours did change:

  • notifications:install and notifications:switch now write NOTIFICATIONS_CSS_FRAMEWORK and NOTIFICATIONS_FRONTEND rather than UI_KIT_CSS and UI_KIT_FRONTEND. Applications that set the UI_KIT_* variables keep working, because those are still read as the fallback.
  • The Livewire component now lists the inbox rather than every notification, matching the Blade view.
  • Sending to a role that does not exist returns a validation error instead of silently sending to the user role.
  • archiveAll now marks unread notifications as read, matching what archiving one at a time already did. Unarchiving such a notification brings it back read rather than unread.
  • sendToRole gained an optional trailing $sendEmail argument, so the send form's email checkbox now applies to role audiences as well as to all users. Existing calls are unaffected.
  • The migration creates the notifications table when the application has none, rather than skipping and leaving archived_at behind for good.

The enabled, auto_mark_read_on_view and confirm_style config keys were removed. None of them were ever read by any code in the package, so removing them changes no behaviour.

License

This package is open-sourced software licensed under the MIT license.