Laravel database notifications in Vue

A notification written to the database is a prop on the next Inertia visit. Live delivery is the Reverb guide, not a second copy of Echo.

For your AI agent

Copies a prompt. Paste it into the AI agent you already have open.

Read https://laravel-vuejs.space/laravel-vue-notifications.md
A notification written to the database is a prop on the next Inertia visit. Live delivery is the Reverb guide, not a second copy of Echo.
Do not poll and subscribe to the same list.
Show the diff. Do not commit.

Next: Live validation with Laravel Precognition and Vue

On this page · 7 sections
  1. Store one
  2. Show ten
  3. Where this stops
  4. Recap
  5. FAQ
  6. Next steps
  7. Sources

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; via returns ['database'].
  • Share unreadNotifications()->latest()->limit(10); read notification.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.

What you'll learn

  • How to store a database notification
  • How to share unread notifications with a Vue page
  • How to mark one read safely

Who it's for: Laravel + Vue developers adding in-app notifications

Applies to: Laravel 13 · Inertia 3 · Vue 3

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#

bash
php artisan make:notification PostPublished
php artisan make:notifications-table
php artisan migrate

via returns the database channel only. toArray is the JSON stored on the row. Keep it small. The Vue page will print data.title.

app/Notifications/PostPublished.phpphp
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.

php
$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.

app/Http/Middleware/HandleInertiaRequests.phpphp
'notifications' => $request->user()
    ?->unreadNotifications()
    ->latest()
    ->limit(10)
    ->get(['id', 'data', 'created_at']) ?? [],
resources/js/components/NotificationList.vuevue
<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.

routes/web.phpphp
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

bash
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.

Next steps#

Sources#

Found this useful?

Ask questions, share what you built.

Join thousands of Laravel and Vue developers in our Facebook group, or get new posts in your Telegram feed. Sharing this article helps too.

Keep reading

Newsletter

Get the best of Laravel & Vue, once a month.

One short email with new guides, release notes worth reading and hand-picked community links. No spam, unsubscribe any time.