Drop-in in-app database notifications and notification preferences for Laravel. Ships
the notifications table used by Laravel's database notification channel, a
notification_preferences column + trait for your User, sane preference defaults, and a
mark-as-read endpoint. Front-end agnostic, so it fits any theme.
devdojo/notifications is one of the feature packages bundled by
devdojo/foundation, but it works perfectly
well standalone in any Laravel app.
- Requirements
- How it works
- Installation
- Configuration
- Wiring your User model
- Sending & reading notifications
- The mark-as-read endpoint
- Notification preferences
- Using with DevDojo Foundation
- Configuration reference
- FAQ / troubleshooting
- Testing
| Requirement | Notes |
|---|---|
PHP ^8.2 |
|
Laravel ^10 / ^11 / ^12 |
The package is front-end agnostic: it provides the storage, preferences, and endpoint, and leaves the notification UI (bells, dropdowns, popups) to your application/theme.
┌──────────────────────────────────────────────────────────────┐
│ devdojo/notifications │
│ │
│ notifications table (Laravel database channel, UUID ids) │
│ users.notification_preferences (json) │
│ │
│ Trait added to your User: │
│ • HasNotificationPreferences (notificationPreference()) │
│ │
│ POST /notification/read/{id} • preference defaults config │
└──────────────────────────────────────────────────────────────┘
- Notifications are plain Laravel database notifications — send them with
$user->notify(...)and read them with$user->notifications/$user->unreadNotifications. The package provides the table. - Each user gets a
notification_preferencesJSON column; theHasNotificationPreferencestrait reads it with per-key fallbacks to the package defaults.security_alertscan never be turned off. POST /notification/read/{id}removes a notification for the signed-in user — the "dismiss" action for a notification list UI.
composer require devdojo/notificationsPublish the migrations and config, then migrate:
php artisan vendor:publish --tag=notifications:migrations
php artisan vendor:publish --tag=notifications:config
php artisan migrateMigrations are publish-only (not auto-loaded) so the
notificationstable and theusers.notification_preferencescolumn live in your app'sdatabase/migrationsand are yours to edit.
The preferences migration adds the column
->after('avatar')— if youruserstable has noavatarcolumn, adjust the published migration before migrating.
Then wire your User model.
Publishing notifications:config writes config/devdojo/notifications/settings.php
(config key devdojo.notifications.settings):
return [
'default_preferences' => [
'email_notifications' => true,
'marketing_emails' => true,
'product_updates' => true,
'blog_notifications' => false,
'security_alerts' => true,
],
];These are the values notificationPreference() falls back to when a user has not stored
their own preferences yet.
Add the HasNotificationPreferences trait to your User model (alongside Laravel's own
Notifiable):
use Devdojo\Notifications\Traits\HasNotificationPreferences;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use HasNotificationPreferences, Notifiable;
}The trait automatically casts notification_preferences to an array and adds:
$user->notificationPreference('marketing_emails'); // bool — stored value or default
$user->notificationPreference('custom_key', 'fallback'); // with an explicit fallbackNothing package-specific here — use Laravel's database channel:
use Illuminate\Notifications\Notification;
class ProjectShipped extends Notification
{
public function via(object $notifiable): array
{
return ['database'];
}
public function toDatabase(object $notifiable): array
{
return ['message' => 'Your project has shipped!'];
}
}
$user->notify(new ProjectShipped);
$user->notifications; // all notifications, newest first
$user->unreadNotifications; // read_at is null
$user->unreadNotifications->markAsRead();Respect a preference before sending:
if ($user->notificationPreference('product_updates')) {
$user->notify(new ProductUpdate($release));
}| Method | URI | Name | Middleware |
|---|---|---|---|
POST |
/notification/read/{id} |
wave.notification.read |
web, auth |
It looks the notification up on the signed-in user only and deletes it (this package treats "read" as "dismissed"). The response is JSON:
// e.g. when a notification is dismissed from a dropdown list
fetch(`/notification/read/${notification.id}`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Content-Type': 'application/json' },
body: JSON.stringify({ listid: 'notification-7' }),
});{ "type": "success", "message": "Marked Notification as Read", "listid": "notification-7" }An unknown id — or an id belonging to another user — returns
{ "type": "error", "message": "Could not find the specified notification." }.
Preferences are a JSON object on the user keyed by feature, e.g.:
$user->notification_preferences = [
'email_notifications' => true,
'marketing_emails' => false,
];
$user->save();notificationPreference($key, $default = null) resolves in this order:
security_alertsis alwaystrue— it cannot be disabled, even if stored asfalse.- The user's stored value for
$key, if present. - The
$defaultyou pass, if any. - The package default from
devdojo.notifications.settings.default_preferences.
The keys are yours to define — store any keys your app needs and add matching defaults to the published config.
When the devdojo/foundation metapackage is
installed, notifications self-gate on the feature flag:
// config/foundation.php
'features' => [
'notifications' => true, // flip to false (or toggle at /foundation/setup) to disable
],When notifications is disabled the notification/read/{id} route is not registered. The
config, trait, and migrations remain available, so toggling is lossless.
Standalone (no Foundation present), the flag is absent and notifications default to on.
return [
'default_preferences' => [
'email_notifications' => true,
'marketing_emails' => true,
'product_updates' => true,
'blog_notifications' => false,
'security_alerts' => true, // always treated as enabled
],
];| Tag | Publishes to |
|---|---|
notifications:config |
config/devdojo/notifications/settings.php |
notifications:migrations |
database/migrations |
Marking as read deletes the notification — why?
The endpoint powers dismissable in-app notification lists: once dismissed, the row is
removed. If you want persistent read state instead, call markAsRead() on the
notification (Laravel sets read_at) and skip the endpoint.
Where is the notification bell / dropdown UI?
The package is headless on the front-end. Render $user->notifications however your
app prefers; in the Wave starter kit the UI ships as theme partials you can edit.
Where does the notifications table come from?
It's a publish-only migration — run
php artisan vendor:publish --tag=notifications:migrations && php artisan migrate.
The preferences migration fails on after('avatar').
The published migration positions the column after avatar. If your users table has no
such column, remove the ->after('avatar') call in your published copy.
composer install
composer testThe suite runs on Pest + Orchestra Testbench against an in-memory sqlite database.
MIT © DevDojo