Building a Notification System with Laravel Notifications
Almost every app needs to tell users that something happened: an order shipped, a payment failed, someone mentioned them. The naive approach scatters mail calls, database inserts, and Slack webhooks all over your codebase. Laravel Notifications consolidate all of that into a single class per event that can deliver across multiple channels at once. This tutorial builds a complete notification system.
Step 1: Generate a Notification
php artisan make:notification InvoicePaid
This creates app/Notifications/InvoicePaid.php. A notification is a small class that knows what to say and which channels to say it on.
Step 2: Choose the Delivery Channels
The via() method returns the channels a notification should go out on. You can decide dynamically based on the notifiable's preferences:
class InvoicePaid extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(public Invoice $invoice) {}
public function via(object $notifiable): array
{
// Respect each user's notification preferences
return $notifiable->prefers_sms
? ['mail', 'database', 'vonage']
: ['mail', 'database'];
}
}
Implementing ShouldQueue means every channel is delivered in the background. Notifications that send email or hit external APIs should always be queued so they never block the request.
Step 3: Define Each Channel's Content
Each channel has its own method that shapes the message for that medium. The mail channel uses the same fluent builder as Mailables:
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject("Payment received for invoice #{$this->invoice->number}")
->greeting("Thanks, {$notifiable->first_name}!")
->line("We received your payment of \${$this->invoice->total}.")
->action('View Invoice', route('invoices.show', $this->invoice))
->line('Thank you for your business.');
}
public function toArray(object $notifiable): array
{
// Stored as JSON in the notifications table
return [
'invoice_id' => $this->invoice->id,
'amount' => $this->invoice->total,
'message' => "Invoice #{$this->invoice->number} was paid.",
];
}
The toArray() data is what gets stored for the database channel and shown in your in-app notification bell.
Step 4: Set Up the Database Channel
For in-app notifications you need the notifications table:
php artisan make:notifications-table
php artisan migrate
Make sure your User model uses the Notifiable trait (new apps already do):
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
}
Step 5: Send the Notification
There are two ways to send. Use the notifiable directly, or the Notification facade for on-demand and bulk sends:
// To a single user
$user->notify(new InvoicePaid($invoice));
// To many at once
Notification::send($admins, new InvoicePaid($invoice));
// On-demand, to someone who is not a stored model
Notification::route('mail', 'accounting@example.com')
->notify(new InvoicePaid($invoice));
Step 6: Display In-App Notifications
The Notifiable trait gives you relationships for reading stored notifications. Build a notification bell by querying the unread ones:
// In a controller
$unread = $request->user()->unreadNotifications;
// Mark them as read when the user opens the panel
$request->user()->unreadNotifications->markAsRead();
// Or mark a single one
$notification = $user->notifications()->find($id);
$notification->markAsRead();
In your Blade view:
@foreach (auth()->user()->unreadNotifications as $note)
<div class="notification">
{{ $note->data['message'] }}
<span>{{ $note->created_at->diffForHumans() }}</span>
</div>
@endforeach
Step 7: Add a Slack or SMS Channel Later
The payoff of this architecture shows up when requirements change. Want to ping a Slack channel when a high-value invoice is paid? Add 'slack' to via() and write one method — every place that already sends the notification picks it up automatically:
public function toSlack(object $notifiable): SlackMessage
{
return (new SlackMessage)
->success()
->content("💰 Invoice #{$this->invoice->number} paid: \${$this->invoice->total}");
}
No hunting through controllers, no duplicated logic. One class owns everything about "what happens when an invoice is paid," across every channel.
That is the whole value of Laravel Notifications: a single source of truth per event, queued delivery, multi-channel output, stored in-app history, and trivial extensibility. Build your next user-facing event this way and adding a channel six months from now is a five-minute change instead of a refactor.