[Complete Practical Guide] Building a Notification Center with Laravel — Database Notifications, Read Management, Email Integration, Real-Time Notifications, and Accessible Notification UI
What You Will Learn in This Article
- Basic design for in-app notifications using Laravel Notifications
- Notification storage, listing, and read/unread management with Database Notifications
- How to use email notifications, in-app notifications, and real-time notifications appropriately
- Designing notification types, priority, notification settings, opt-out, and aggregation
- Accessible UI for notification centers, badges, toasts, and empty states
- Practical patterns for queues, retries, idempotency, audit logs, and testing
Intended Readers
- Beginner to intermediate Laravel engineers who want to build in-app notifications and read management, not just email notifications
- SaaS, admin dashboard, and business system developers who want to implement a per-user notification center
- Tech leads who need to organize notification design and operations as notification types increase
- Designers, QA, and accessibility staff who want to improve notification badges, toasts, and notification lists
Accessibility Level: ★★★★★
Notifications are UI elements that attract users’ attention. That is why it is important not to interrupt operations with excessive display or automatic updates, not to communicate notification counts or states by color alone, and to use role="status" and aria-live appropriately. This article designs notifications not as “information only visible users can receive,” but as “state changes that all users can understand.”
1. Introduction: Notifications Are Not Just for “Informing,” but for Guiding Users to Action
Web applications need notifications in many situations: a comment has arrived, an application has been approved, an invoice has been issued, an export has completed, security settings have changed, or a deadline is approaching. Delivering this information to users makes it easier for them to understand what action to take next.
However, too many notifications can have the opposite effect. If everything becomes a notification, users stop looking at them. If important notifications and lightweight notifications look the same, truly necessary information gets buried. In addition, UIs such as toasts that disappear too quickly, badges that indicate state only through red color, or notifications whose content is not understandable through screen readers create accessibility issues.
Laravel provides a powerful mechanism called Notifications. It supports multiple channels such as mail, database, Slack, and Broadcast, and helps you build a structure for organizing notifications. This article summarizes practical ways to build a notification center in Laravel, covering the basics, read management, email integration, real-time notifications, and accessible UI.
2. Organize Notification Types: Do Not Treat Every Notification the Same
Before building a notification center, first organize notification types. Notifications should be handled differently depending on their importance and purpose.
2.1 Informational Notifications
Examples:
- A new comment has arrived
- Report generation has completed
- Profile has been updated
These are relatively lightweight notifications. In many cases, in-app notifications alone are enough.
2.2 Action-Required Notifications
Examples:
- Approval of an application is required
- Payment method needs to be updated
- Response to an invitation is required
These notifications require user action. They need a path from the notification list to a detail page or action screen.
2.3 Important / Security Notifications
Examples:
- Password was changed
- Two-factor authentication was disabled
- Suspicious login was detected
These are notifications that should often be combined with email notifications. In-app notifications alone may be missed.
2.4 Scheduled / Aggregated Notifications
Examples:
- Weekly report
- Summary of pending tasks
- Comment digest
If notifications become too frequent, aggregating them instead of sending individual notifications is often more considerate.
When designing notifications, deciding whether something should be shown immediately, whether email is also needed, whether it only needs to be visible in a list, and whether action is required helps prevent notification fatigue.
3. Basics of Laravel Notifications
In Laravel, you create notification classes and send them to Notifiable models such as users. In many cases, the User model includes the Notifiable trait.
Create a notification class.
php artisan make:notification ReportGeneratedNotification
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class ReportGeneratedNotification extends Notification
{
use Queueable;
public function __construct(
public int $reportId,
public string $title
) {}
public function via(object $notifiable): array
{
return ['database'];
}
public function toArray(object $notifiable): array
{
return [
'type' => 'report.generated',
'report_id' => $this->reportId,
'title' => $this->title,
'message' => 'The report has been generated.',
'url' => route('reports.show', $this->reportId),
];
}
}
Sending side:
$user->notify(new ReportGeneratedNotification(
reportId: $report->id,
title: $report->title
));
In this example, the notification is stored in the database channel. In other words, it can be displayed in the notification center list.
4. Preparing Database Notifications
To store in-app notifications, create the notifications table.
php artisan notifications:table
php artisan migrate
This creates the notifications table. Notification data is stored as JSON, and the read state is managed with read_at.
You can retrieve a user’s notification list as follows.
$notifications = auth()->user()
->notifications()
->latest()
->paginate(20);
To retrieve only unread notifications:
$unreadNotifications = auth()->user()
->unreadNotifications()
->latest()
->get();
To mark a notification as read:
$notification = auth()->user()
->notifications()
->whereKey($id)
->firstOrFail();
$notification->markAsRead();
To mark all notifications as read:
auth()->user()
->unreadNotifications
->markAsRead();
Database Notifications are very easy to use as the foundation of a notification center.
5. Designing Notification Data: Make It Easy to Display Later
You can put arbitrary information into notification data. However, if you put everything in, the display side becomes confusing. In practice, standardizing the structure like the following makes it easier to handle.
[
'type' => 'report.generated',
'title' => 'Report Generated',
'message' => 'You can now view the April sales report.',
'url' => route('reports.show', $report->id),
'severity' => 'info',
'action_label' => 'View report',
]
Recommended fields are:
type: notification typetitle: short headingmessage: body texturl: link to details or actionseverity: info / success / warning / danger, etc.action_label: link or button labelmeta: supplementary information when needed
This structure makes it easier to reuse data for notification lists, toasts, email notifications, and more.
One important caution is not to put too much confidential information into notification data. The notification table may later be displayed, logged, or handled in admin screens. If the body includes personal or secret information, keep it to the minimum necessary.
6. Notification Center Screen: Organize List, Read State, and Empty State
A notification center should not merely list notifications; it should make states clear.
<section aria-labelledby="notifications-title">
<h1 id="notifications-title" class="text-2xl font-semibold">
Notifications
</h1>
<p role="status" aria-live="polite" class="mt-2 text-sm">
You have {{ auth()->user()->unreadNotifications()->count() }} unread notifications.
</p>
@if($notifications->isEmpty())
<div class="border rounded p-4 mt-6">
<h2 class="text-lg font-semibold">There are no notifications</h2>
<p class="mt-2">
New announcements will appear on this screen.
</p>
</div>
@else
<ul class="mt-6 divide-y">
@foreach($notifications as $notification)
@php
$data = $notification->data;
$isUnread = is_null($notification->read_at);
@endphp
<li class="py-4">
<article aria-labelledby="notification-{{ $notification->id }}-title">
<div class="flex items-start justify-between gap-4">
<div>
<h2 id="notification-{{ $notification->id }}-title" class="font-semibold">
{{ $data['title'] ?? 'Notification' }}
@if($isUnread)
<span class="ml-2 rounded bg-blue-100 px-2 py-1 text-sm text-blue-900">
Unread
</span>
@endif
</h2>
<p class="mt-1">
{{ $data['message'] ?? '' }}
</p>
<p class="mt-1 text-sm text-gray-600">
{{ $notification->created_at->diffForHumans() }}
</p>
</div>
@if(!empty($data['url']))
<a href="{{ $data['url'] }}" class="underline">
{{ $data['action_label'] ?? 'View details' }}
</a>
@endif
</div>
</article>
</li>
@endforeach
</ul>
<div class="mt-6">
{{ $notifications->links() }}
</div>
@endif
</section>
In this example, the unread state is shown not only with blue color, but also with the text “Unread.” It is important not to rely on color alone.
Also, the empty state does not end with only “There are no notifications”; it explains where future notifications will appear.
7. Read Management: Separate Automatic Read and Manual Read
There are two main approaches to read management.
7.1 Mark as Read When Displayed
Mark notifications as read when the user opens the notification detail page or clicks the notification link.
This is natural, but marking as read just because the list was displayed requires caution. The user may not have actually read it.
7.2 Mark as Read Manually
Provide a “Mark as read” button or “Mark all as read” action.
This makes it easier for users to manage state themselves, although it adds a little more interaction.
In practice, marking as read when the notification link is opened and also providing “Mark all as read” when needed is often easy to handle.
Route::patch('/notifications/{id}/read', function (string $id) {
$notification = auth()->user()
->notifications()
->whereKey($id)
->firstOrFail();
$notification->markAsRead();
return back()->with('status', 'The notification has been marked as read.');
})->name('notifications.read');
UI side:
<form method="POST" action="{{ route('notifications.read', $notification->id) }}">
@csrf
@method('PATCH')
<button type="submit" class="underline">
Mark as read
</button>
</form>
After marking as read, it is helpful to announce the result with role="status".
8. Notification Badges: Communicate State with Numbers and Labels
When displaying notification badges in the header or sidebar, a red dot alone does not communicate meaning. Include numbers and text.
<a href="{{ route('notifications.index') }}" class="relative inline-flex items-center">
<span>Notifications</span>
@if($unreadCount > 0)
<span class="ml-2 rounded-full bg-red-600 px-2 py-1 text-sm text-white">
{{ $unreadCount }}
</span>
<span class="sr-only">
You have {{ $unreadCount }} unread notifications
</span>
@endif
</a>
Visible users receive the number visually, while screen reader users receive the explanation through sr-only.
Rather than assuming “the red dot makes it obvious,” it is important to communicate in text how many unread notifications there are.
9. Toast Notifications: Do Not Overuse Them, and Be Careful How They Disappear
Toast UI is sometimes used for completion messages or lightweight notifications. It is convenient, but there are accessibility considerations.
- Do not make it disappear too quickly
- Allow it to be dismissed by keyboard as well as mouse
- Do not use toasts alone for important notifications
- Do not automatically steal focus
- Use
role="status"orrole="alert"appropriately
Example of a success notification:
@if(session('status'))
<div role="status" aria-live="polite" class="border rounded p-3 mb-4">
{{ session('status') }}
</div>
@endif
For serious errors:
@if(session('error'))
<div role="alert" class="border rounded p-3 mb-4">
{{ session('error') }}
</div>
@endif
As a general rule, low-importance completion notifications can use role="status", while urgent failures can use role="alert".
However, role="alert" may interrupt screen reader output strongly, so avoid overusing it.
10. Choosing Between Email Notifications and In-App Notifications: Judge by Importance and Required Action
Some notifications only need to be in-app, while others should also be delivered by email.
Examples Where In-App Notification Alone Is Enough
- A comment was added
- Report generation completed
- A minor setting change completed
Examples Where Email Should Also Be Sent
- Password was changed
- Invoice was issued
- Security settings were changed
- Invitation was sent
- There is an important approval request
In Laravel Notifications, you can switch channels with via().
public function via(object $notifiable): array
{
return ['database', 'mail'];
}
In email bodies as well, use specific subjects, short body text, and clear link text, just like in the notification center.
For example, instead of “click here,” use wording that makes the action clear, such as “View invoice.”
11. Notification Settings: Give Users Room to Choose
As notifications increase, users need a design that lets them choose how they receive them. Email notifications in particular can lead to unsubscribes and dissatisfaction if there are too many unnecessary messages.
Examples of notification settings:
- In-app notifications: always receive
- Email notifications: important ones only
- Digest notifications: daily or weekly
- Security notifications: cannot be disabled
- Marketing notifications: optional and can be unsubscribed
Example DB design:
notification_preferences
- user_id
- type
- channel
- enabled
Check the setting before sending notifications.
if ($user->wantsNotification('report.generated', 'mail')) {
$user->notify(new ReportGeneratedNotification($report->id, $report->title));
}
Not everything needs to be user-configurable, but as notifications increase, a settings screen becomes important.
12. Notification Aggregation: Prevent Notification Fatigue
If many notifications of the same type arrive in a short period, users become fatigued.
For example, if 10 comments are added, it may be more considerate to send “You have 10 new comments” rather than sending 10 separate notifications.
Aggregation ideas:
- Combine same-type notifications within a short period
- Send regular digest notifications
- Show only unread count in the badge
- Allow details to be checked in the list
In practice, you do not need to create complex aggregation from the beginning. Once notifications start increasing, it is safer to look at logs and usage data, then aggregate frequently occurring notifications first.
13. Queuing: Notification Delivery Should Usually Be Asynchronous
Email and external notifications slow down screen responses if sent synchronously. Use Queueable in notification classes and implement ShouldQueue when needed.
use Illuminate\Contracts\Queue\ShouldQueue;
class ReportGeneratedNotification extends Notification implements ShouldQueue
{
use Queueable;
// ...
}
This allows notification delivery to be processed by the queue.
In operations, monitor notification queue delays and failures. If notifications are delayed, users may feel as if processing has not finished.
14. Idempotency: Design to Avoid Sending the Same Notification Twice
When using queues or events, the same notification may be sent multiple times. For important notifications, duplicate prevention should be considered.
Examples of approaches:
- Keep a sent flag per notification target
- Do not create a notification if an unread notification with the same
typeand target ID already exists - Use cache locks to prevent short-term duplication
Example:
$exists = $user->notifications()
->where('type', ReportGeneratedNotification::class)
->where('data->report_id', $report->id)
->exists();
if (! $exists) {
$user->notify(new ReportGeneratedNotification($report->id, $report->title));
}
This may be unnecessary while notification volume is small, but it is especially important for billing and security notifications.
15. Real-Time Notifications: Convenient, but Do Not Interrupt User Operations
Broadcasting can deliver notifications to the screen in real time.
However, real-time notifications require care. If the screen updates on its own, it may interfere with user operations.
Basics of accessible real-time notifications:
- Do not move focus automatically
- Update notification count in text
- Use
aria-live="polite" - Consider
role="alert"only for urgent notifications - Do not make important information exist only in toasts
Example:
<div id="notification-live-region" role="status" aria-live="polite" class="sr-only"></div>
When a real-time notification is received, insert a short message into this area.
You have 1 new notification.
This communicates the state change not only through a visual badge update, but also through screen reader output.
16. Testing the Notification Center: Protect Storage, Read State, and Display
Notification features have many states, so they are worth protecting with tests.
16.1 Test That a Notification Is Stored
public function test_report_generated_notification_is_stored()
{
$user = User::factory()->create();
$user->notify(new ReportGeneratedNotification(
reportId: 1,
title: 'Monthly Report'
));
$this->assertDatabaseHas('notifications', [
'notifiable_id' => $user->id,
'notifiable_type' => User::class,
]);
}
16.2 Test Marking as Read
public function test_user_can_mark_notification_as_read()
{
$user = User::factory()->create();
$user->notify(new ReportGeneratedNotification(
reportId: 1,
title: 'Monthly Report'
));
$notification = $user->notifications()->first();
$this->actingAs($user)
->patch(route('notifications.read', $notification->id))
->assertRedirect();
$this->assertNotNull($notification->fresh()->read_at);
}
16.3 Test That Users Cannot Mark Other Users’ Notifications as Read
public function test_user_cannot_mark_others_notification_as_read()
{
$user = User::factory()->create();
$other = User::factory()->create();
$other->notify(new ReportGeneratedNotification(
reportId: 1,
title: 'Monthly Report'
));
$notification = $other->notifications()->first();
$this->actingAs($user)
->patch(route('notifications.read', $notification->id))
->assertNotFound();
}
Notifications are information tied to individuals, so always verify that users cannot manipulate other people’s notifications.
17. Common Pitfalls and How to Avoid Them
17.1 Too Many Notifications
If everything becomes a notification, important notifications get buried.
Organize notification types and importance, and aggregate them when necessary.
17.2 Unread Badge Uses Only Color
A red dot alone does not communicate meaning.
Show unread count with numbers and text.
17.3 Toasts Disappear Too Quickly
Notifications that disappear before users can finish reading are unfriendly.
Important information should also remain in the notification center.
17.4 Email Notifications and In-App Notifications Differ Too Much
This confuses users.
Standardize basic notification data fields.
17.5 Users Can Manipulate Other People’s Notifications
This is an authorization flaw.
When retrieving notifications, always search through the logged-in user’s notifications.
17.6 Real-Time Notifications Interrupt Operations
Do not steal focus. Communicate quietly with aria-live="polite".
18. Checklist for Distribution
Design
- [ ] Notification types and importance are organized
- [ ] In-app, email, and real-time notification usage is defined
- [ ] Common notification data fields exist
- [ ] Notification settings and opt-out policy exist
Database Notifications
- [ ] notifications table is created
- [ ] Notification list exists
- [ ] Read/unread management exists
- [ ] Users cannot manipulate other users’ notifications
UI / Accessibility
- [ ] Notification count is shown in text
- [ ] Unread state is not shown by color alone
- [ ] Empty state explanation exists
- [ ] Completion notifications use
role="status" - [ ] Important failures use
role="alert" - [ ] Real-time updates do not steal focus
Operations
- [ ] Notification delivery is queued
- [ ] Duplicate delivery of important notifications is prevented
- [ ] Aggregation policy exists for increased notification volume
- [ ] Notification failures are monitored
Testing
- [ ] Notification storage test exists
- [ ] Mark-as-read test exists
- [ ] Test exists to prevent manipulating other users’ notifications
- [ ] Notification list display test exists
19. Conclusion
Laravel Notifications are a powerful mechanism that can go beyond sending email and expand into in-app notifications, notification centers, read management, and real-time updates. What matters is not increasing the number of notifications, but delivering the necessary information at the necessary time in an understandable way.
Start by storing notifications with Database Notifications, then build the notification list, unread badge, and read management. Next, combine important notifications with email and consider notification settings and aggregation. Real-time notifications are convenient, but use them modestly so they do not interrupt operations.
Notifications are guideposts that help users take action. By avoiding reliance on color alone, communicating states with text, considering screen reader output, and ensuring important notifications are not buried, you can create a trustworthy notification experience.

