Skip to content

Start building your next app or website — with AI, right in your browser.

Visit devdojo.com
Docs
Notifications

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 notifications table with the standard Laravel database-notification schema (UUID primary key, polymorphic notifiable, JSON data, read_at).
  • A notification_preferences JSON column on users, plus the HasNotificationPreferences trait to read individual preferences with sensible fallbacks.
  • A configurable set of default preferences, including a security_alerts channel 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 User model needs Laravel's Notifiable trait — the package builds on the framework's database channel 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_preferences and exposing them in your account settings form; gate each notify() call with notificationPreference().
  • 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.

© 2026 DevDojo Edit this page