Laravel 13 Notifications: One System for Email, Database, and Slack
A single notification class with a via() method can send the order confirmation email, store the alert in the database for the app's bell icon, and ping the team on Slack. That is how the Laravel 13 notification system works, and this guide builds it from scratch around a real store order.
What Notifications Are in Laravel 13
Laravel 13's notification system is a central layer that separates the act of notifying (a business event: an order was confirmed) from the delivery channels (email, database, Slack, SMS, broadcast). Instead of writing three different blocks of logic every time something happens, you define one notification class per event and Laravel delivers it through every channel.
One Class, Many Channels: The via() Method
Each notification is a PHP class that declares a via() method. That method returns an array with the channels the message should be delivered through: mail, database, broadcast, slack, vonage, or any community channel. Then, for every channel you declare, the class defines a method that builds the message: toMail() returns a MailMessage, toDatabase() a plain array, toSlack() a Slack message. Adding a new channel does not touch the business logic: you just add one more method.
Notifiable: Which Models Can Receive Notifications
For a model to receive notifications it must use the Notifiable trait. The User model in Laravel's skeletons already includes it, but you can add it to any other model: orders, customers, event guests. The trait provides the notify() method and access to the relationship of notifications stored in the database.
Your First Notification: Order Confirmation by Email
The running example in this guide is a store: when a customer pays for an order, we want to send the confirmation email, store an alert in their in-app inbox, and notify the team on Slack. Start with the channel everyone expects: email.
php artisan make:notification and the Notification Structure
The command php artisan make:notification OrderConfirmed generates the base class in app/Notifications. The minimal structure is the via() method plus one method per channel. The notification usually receives the data it needs in the constructor, such as the order instance:
class OrderConfirmed extends Notification
{
public function __construct(public Order $order) {}
public function via(object $notifiable): array
{
return ['mail', 'database', 'slack'];
}
}The Blade Email Template with toMail()
The toMail() method builds a MailMessage, chaining a subject, text lines, and actions. The output is rendered with the Blade templates Laravel ships by default and inherits the application's mail theme:
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Order ' . $this->order->number . ' confirmed')
->greeting('Hi, ' . $notifiable->name)
->line('Your order has been confirmed and is being prepared.')
->action('View my order', url('/orders/' . $this->order->id))
->line('Thank you for shopping with us.');
}Sending with notify() and Notification::send()
When the payment is processed, delivery is triggered. From the receiving model, $user->notify(new OrderConfirmed($order)); for several recipients, Notification::send($users, new OrderConfirmed($order)) loops over the collection and delivers to each one. Both paths use the same internal mechanism, so you can pick whichever reads better at each point of the code.
The database Channel: In-App Alerts
The database channel stores the notification in a table and the app shows it in a bell icon or an alerts panel. It is useful for things the user should see when they come back, not in their inbox: status changes, support messages, results of an async action.
The notifications Table and Its Default Migration
Since Laravel 11, the create_notifications_table migration is included in the skeleton and runs with your regular migrations. If your project was created earlier or the table is missing, the php artisan make:notifications-table command generates it. The table stores the notification type, the data as JSON, and read markers.
Marking as Read and Querying Unread Alerts
The Notifiable trait exposes unreadNotifications and readNotifications as relationships. To render the bell's counter, $user->unreadNotifications->count() is enough, and when the panel opens you mark them as read with $notification->markAsRead(). The array returned by toDatabase() is available as $notification->data in the view.
Slack and the Rest of the Channels: broadcast, Vonage, and Community
Beyond the core channels, Laravel integrates third-party services through packages that follow the same contract: a via() method listing the channel and a toXxx() method building the message.
Slack with laravel/slack-notification-channel
The official laravel/slack-notification-channel package is installed with Composer and configured by pointing at an incoming Slack webhook. In the notification, toSlack() returns a SlackMessage with text, attachments, or buttons. The team receives the order alert in the configured channel without touching the store's logic.
When to Use broadcast (Reverb) and SMS (Vonage)
The broadcast channel publishes the notification in real time over WebSockets (Reverb in Laravel's current stack) to show it instantly in the UI. vonage sends SMS, useful for urgent confirmations or customers without the app. The practical rule: email and database for what can wait, broadcast for what is immediate, and SMS only when the phone is the only reliable channel. The community adds more channels, like Telegram, with the same pattern.
On-Demand Notifications for Users Without a Model
Sometimes the recipient has no account: a customer who checks out as a guest or an email address collected in a form. On-demand notifications solve this without creating models: Notification::route('mail', 'guest@example.com')->notify(new OrderConfirmed($order)). The route() method defines the channel and the destination, and you can chain several routes to send by mail and SMS at the same time.
Queues: Keep Sending from Blocking the Response
Sending an email or calling a Slack webhook inside the request lifecycle adds latency and a failure point. The answer is to queue the notification so a worker processes it in the background.
ShouldQueue and the Worker Flow
By making the class implement ShouldQueue, Laravel places the notification on the default queue and the send happens when a worker picks it up, without the client waiting. The rest of the class does not change: the toXxx() methods run inside the worker, so it is wise to pass the data you need through the constructor rather than objects that may become stale.
Delaying Sends with ->delay() and the #[Delay] Attribute in Laravel 13.4+
To send later, the classic chained call is ->delay(now()->addMinutes(10)). Since Laravel 13.4, the #[Delay] attribute generalizes this delay: you declare it on the class or constructor and Laravel honors it for both jobs and notifications, avoiding scattered delay() calls across the codebase. It is the natural choice for reminders or messages that must wait until the order is confirmed.
Avoiding Duplicates and Rate Limiting Sends
Two protections keep the system from firing wildly: one against duplicates and one against bursts of sends.
ShouldBeUnique and Notification Throttling
If an event can fire several times (a repeated payment webhook), the notification implements ShouldBeUnique and Laravel does not enqueue a second copy while the first is still pending or processing. To limit the rate per channel and recipient, throttling uses a cache store and defines how many sends are allowed per time window, handy for campaigns or bulk alerts.
Testing with Notification::fake()
Tests should never touch the real driver. With Notification::fake() the send is replaced and assertions run without delivering anything:
Notification::fake();
$user->notify(new OrderConfirmed($order));
Notification::assertSentTo($user, OrderConfirmed::class);
Notification::assertNotSentTo($otherUser, OrderConfirmed::class);Besides assertSentTo, the fake exposes assertSentOnDemand for on-demand notifications and assertNothingSent for negative cases, so you can cover the whole order confirmation flow with confidence.
Conclusion
Laravel 13's notification system turns a cross-cutting problem (notifying through several channels) into one class per event with clear per-channel methods. Starting with the confirmation email, adding the database bell, Slack for the team, on-demand for guests, and queues to avoid blocking the response covers 90% of a real application's needs. If you want to dig into the transport behind these sends, this blog already has guides on queues and jobs with Horizon and on Reverb for real-time updates.