You want a bell with unread notifications, and every tutorial jumps straight to websockets. Start smaller: a database notification, ten unread rows in a prop, and a “mark as read” POST. Live updates can come later.
TL;DR
make:notification+make:notifications-table;viareturns['database'].- Share
unreadNotifications()->latest()->limit(10); readnotification.data.title. - Mark read through the current user’s
notifications()so other ids 404. - Database notifications do not push to an open page; that is Reverb.
Heads up: stored, not pushed
The database channel writes a row the page can read on the next visit. It does not push that row to an open browser. If the bell has to update while the user sits on the page, that is Reverb. This page stops at the stored notification and the Vue list.
The app those pages belong to is Inertia, a SPA, or Nuxt.
Store one#
php artisan make:notification PostPublished
php artisan make:notifications-table
php artisan migratevia returns the database channel only. toArray is the JSON stored on the row. Keep it small. The Vue page will print data.title.
public function via(object $notifiable): array
{
return ['database'];
}
public function toArray(object $notifiable): array
{
return [
'title' => $this->post->title,
];
}Pass the post into the constructor and call notify on the user who should see it. Notifiable is already on the starter kit’s User.
$user->notify(new PostPublished($post));Tip: one channel at a time
Mail, Slack, and broadcast drivers are other via values. Leave them out until you mean to send them. A broadcast notification needs the queue and the Reverb process from that guide. Do not half-configure Echo here.
Show ten#
Share the unread rows from HandleInertiaRequests or from the controller that renders the layout’s data. Limit the query. An unbounded notifications relation will grow into the props.
'notifications' => $request->user()
?->unreadNotifications()
->latest()
->limit(10)
->get(['id', 'data', 'created_at']) ?? [],<ul>
<li v-for="notification in notifications" :key="notification.id">
{{ notification.data.title }}
</li>
</ul>Gotcha: data is not the model
data is the array from toArray, decoded. It is not the Eloquent model. If the title is missing, the notification class returned different keys than the template reads.
Mark one read with a normal POST. Scope it to the current user so an id from someone else 404s.
Route::post('/notifications/{id}/read', function (Request $request, string $id) {
$notification = $request->user()
->notifications()
->whereKey($id)
->firstOrFail();
$notification->markAsRead();
return back();
})->middleware('auth');The Vue control is an Inertia Link or Form with method="post". After back(), the shared query hides the row because it is no longer unread.
Send yourself one
php artisan tinker --execute="App\\Models\\User::first()->notify(new App\\Notifications\\PostPublished(App\\Models\\Post::first()));"Reload the page: the title appears in the list. Mark it read and it disappears.
Where this stops#
Nothing in this page opens a websocket. Polling router.reload({ only: ['notifications'] }) works and is the partial reload guide. A live bell is Reverb. Pick one. Do not poll and subscribe to the same list.
Recap#
- Database channel first; it is a row, not a push.
- Limit the shared query to ten unread rows.
- Scope “mark as read” to the current user.
- Poll with a partial reload or go live with Reverb, not both.
FAQ#
Do Laravel database notifications update the Vue page live?
No. They are stored rows read on the next visit. Use Reverb, or poll with a partial reload, for live updates.
Why is notification.data.title undefined?
data holds whatever toArray returned. Make the template read the same keys the notification class stores.
How do I stop users marking other users’ notifications read?
Look the id up through $request->user()->notifications() so anyone else’s id returns 404.



