Apps (Coming Soon) 28 / 32
Notifications
In-app database notifications and per-user notification preferences for Laravel — the notifications table, a preferences trait with an always-on security channel, and a mark-as-read endpoint you wire into your own inbox UI.
devdojo/notifications is the thin, opinionated layer DevDojo apps use for in-app notifications. Rather than reinventing Laravel's native notification system — the Notifiable trait and the database channel — it builds directly on it and adds the two pieces the framework leaves to you: a ready-made notifications table and a per-user preferences column with a small trait to read it. It ships as a feature package of foundation and works standalone in any Laravel app.
Overview
The package is intentionally small. It provides:
- A
notificationstable with the standard Laravel database-notification schema (UUID primary key, polymorphicnotifiable, JSONdata,read_at). - A
notification_preferencesJSON column onusers, plus theHasNotificationPreferencestrait to read individual preferences with sensible fallbacks. - A configurable set of default preferences, including a
security_alertschannel that can never be disabled. - A mark-as-read route you call from your own inbox UI.
Sending, storing, and querying notifications is pure Laravel: you send with $user->notify(...) over the database channel, and read them through $user->notifications / $user->unreadNotifications. This package fills in the schema, the preferences, and the read endpoint.
No notification classes ship in this package. You author your own Illuminate\Notifications\Notification classes and return the database channel from via(). The package owns the storage and preference layer, not the message definitions — which keeps your notifications yours.
Requirements
- PHP 8.2+ and Laravel 10 – 13.
- Your
Usermodel needs Laravel'sNotifiabletrait — the package builds on the framework'sdatabasechannel rather than shipping its own delivery layer.
Installation
composer require devdojo/notifications
Publish the config and migrations, then migrate:
php artisan vendor:publish --tag=notifications:config
php artisan vendor:publish --tag=notifications:migrations
php artisan migrate
This creates the notifications table and adds a nullable notification_preferences JSON column to users.
The preferences migration adds the column after('avatar'). If your users table has no avatar column, edit the published migration before running it, since it lives in your app's database/migrations.
Wiring your User model
Use Laravel's Notifiable trait to send and store notifications, and add HasNotificationPreferences to read preferences:
use Illuminate\Notifications\Notifiable;
use Devdojo\Notifications\Traits\HasNotificationPreferences;
class User extends Authenticatable
{
use Notifiable, HasNotificationPreferences;
}
HasNotificationPreferences hooks Eloquent's initialize… boot convention to cast notification_preferences to an array automatically — no $casts entry needed.
Configuration
config/devdojo/notifications/settings.php holds the defaults used when a user has no stored preferences yet:
return [
'default_preferences' => [
'email_notifications' => true,
'marketing_emails' => true,
'product_updates' => true,
'blog_notifications' => false,
'security_alerts' => true,
],
];
Config is read through the devdojo.notifications.settings key. Add, remove, or re-default keys here to define the notification channels your app offers.
Notification preferences
Read a single preference with the trait helper. It resolves in this order: the user's stored value → an explicit $default you pass → the package default for that key. The security_alerts channel is special — it always returns true, so critical security notices can never be switched off:
$user->notificationPreference('product_updates'); // stored → default
$user->notificationPreference('blog_notifications', true); // with an explicit fallback
$user->notificationPreference('security_alerts'); // always true
Persist changes by writing the array back to the cast column — typically from an account settings form:
$user->update([
'notification_preferences' => array_merge(
$user->notification_preferences ?? [],
['marketing_emails' => false],
),
]);
Gate an outgoing message on a preference before sending:
if ($user->notificationPreference('product_updates')) {
$user->notify(new ProductUpdate($release));
}
The notifications table
The migration matches Laravel's database-notification format, so the built-in database channel writes to it with no extra mapping:
| Column | Type | Notes |
|---|---|---|
id |
uuid (primary) | Notification identifier |
type |
string | The notification class name |
notifiable_id / notifiable_type |
morphs | The recipient (e.g. a User) |
data |
text | JSON payload from the notification's toArray() |
read_at |
timestamp, nullable | Set when marked read via the framework |
created_at / updated_at |
timestamps |
Sending notifications
Author a standard Laravel notification that returns the database channel, then notify a user:
use Illuminate\Notifications\Notification;
class ProjectDeployed extends Notification
{
public function via($notifiable): array
{
return ['database'];
}
public function toArray($notifiable): array
{
return ['title' => 'Deployment finished', 'url' => route('projects.show', $this->project)];
}
}
$user->notify(new ProjectDeployed($project));
Reading and marking notifications
Query notifications through the Notifiable relations, and mark one read through the package route:
| Method | URI | Route name | Middleware |
|---|---|---|---|
POST |
notification/read/{id} |
wave.notification.read |
web, auth |
The endpoint looks the notification up on the authenticated user, deletes it, and returns a JSON result (echoing the posted listid so a front-end list can update in place):
{ "type": "success", "message": "Marked Notification as Read", "listid": 3 }
Read = removed. In this package "mark as read" deletes the notification row rather than stamping read_at, so an inbox built on unreadNotifications reflects the change immediately. If you need a persisted read history, call the framework's markAsRead() from your own UI instead of this endpoint.
Building an inbox UI
The package ships the storage and the read endpoint; the inbox itself is app UI. The DevDojo Platform renders one with a small Livewire component that reads unreadNotifications and marks items read — a pattern you can copy:
class UserNotifications extends Component
{
public function markAsRead($id)
{
auth()->user()->notifications()->where('id', $id)->first()?->delete();
}
public function markAll()
{
auth()->user()->notifications()->get()->each->delete();
}
public function render()
{
$notifications = auth()->user()->unreadNotifications->take(10);
return view('livewire.user-notifications', compact('notifications'));
}
}
Alternatively, POST to the wave.notification.read route from a plain Alpine/JS dropdown and update the list from the JSON response.
How it's wired
NotificationsServiceProvider merges the settings config, registers the two publish tags (notifications:config, notifications:migrations), and — like every foundation package — self-gates its route on the feature flag:
if (! config('foundation.features.notifications', true)) {
return; // the mark-as-read route is not registered
}
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
Standalone (no Foundation present) the flag is absent and notifications default to on. The migrations, config, and HasNotificationPreferences trait are always available regardless of the toggle, so disabling and re-enabling the feature is lossless. The route name (wave.notification.read) and the migration format are kept identical to Wave's originals, so the package is a drop-in replacement in existing Wave apps.
Extension points
- Add channels by adding keys to
default_preferencesand exposing them in your account settings form; gate eachnotify()call withnotificationPreference(). - Combine with the ecosystem — send team-activity notices alongside teams events, or dunning notices from billing webhooks, all through the same in-app inbox.
- Own the UI — because the inbox is app-level, style it however your product needs while reusing the storage, preferences, and read endpoint this package standardizes.
If a platform screen needs a notification primitive this package doesn't have yet — a new channel type, a digest, a persisted read history — add it here so every app on the foundation benefits, rather than forking the behavior into one app.