Build a nice Realtime notification with Laravel Jetstream (InertiaJS / Vue 3 stack).

Build a nice Realtime notification with Laravel Jetstream (InertiaJS / Vue 3 stack).

In this tutorial we are going to build a nice looking notification for InertiaJS admin. Here we are using vue 3. First let us install laravel

composer create-project laravel/laravel .

Let us use SQLite for ease of use. You just have to add this in .env. And don’t forget to remove all other DB_ variables in .env

DB_CONNECTION=sqlite

If you prefer any other database feel free to skip this step.

Laravel Jetstream ​

Laravel Jetstream is a beautifully designed application starter kit for Laravel and provides the perfect starting point for your next Laravel application. Jetstream provides the implementation for your application’s login, registration, email verification, two-factor authentication, session management, API via Laravel Sanctum, and optional team management features. These commands will install Jetstream for us.

composer require laravel/jetstream
php artisan jetstream:install inertia

npm install
npm install @heroicons/vue
npm run build
php artisan migrate

And run php artisan serve in one cli and npm run dev in another to keep running vite and artisan servers.

Now go to this file databaseseedersDatabaseSeeder.php and add these code. This will seed two users (admin@admin.com and admin2@admin.com) in users table.

AppModelsUser::factory()->create([
‘name’ => ‘admin’,
’email’ => ‘admin@admin.com’,
]);

AppModelsUser::factory()->create([
‘name’ => ‘admin2’,
’email’ => ‘admin2@admin.com’,
]);

Now run this command

php artisan migrate:fresh –seed

Now you have two users admin@admin.com and admin2@admin.com in table with password password. you can loigin from one of those accounts.

Now let us build the UI

For UI we are build a notification icon with notification count in the menu. We use ‘resourcesjsLayoutsAppLayout.vue’ file since menu is located there.

Just before <!– Settings Dropdown –> Add this code.

<div class=”relative inline-block cursor-pointer”>
<Dropdown align=”right” width=”96″>
<template #trigger>
<BellIcon class=”h-7 w-7 text-gray-600″ />

<span
class=”absolute bottom-3 left-3 flex items-center justify-center h-5 w-5 rounded-full bg-red-600 text-white text-xs”
>
{{ 2 }}
</span>
</template>

<template #content>
<!– Account Management –>
<div class=”block px-4 py-2 text-xs text-gray-400 w-[350px]”>
Notifications
</div>

<div class=”border-t border-gray-200″ />

<div>
<DropdownLink :href=”route(‘dashboard’)”>
<div class=”block text-xs”>Title</div>
<div>Notification description</div>
</DropdownLink>

<div class=”border-t border-gray-200″ />
</div>

<div>
<DropdownLink :href=”route(‘dashboard’)”>
<div class=”block text-xs”>Title 2</div>
<div>Notification description 2</div>
</DropdownLink>

<div class=”border-t border-gray-200″ />
</div>
</template>
</Dropdown>
</div>

And don’t forget to import BellIcon like this,

import { BellIcon } from ‘@heroicons/vue/24/solid’

Now you will see this in browser,

You can see this changes here in github (Notice that branch build_ui is for this section only)
https://github.com/vimuths123/notification/tree/build_ui

Build database table

Now let us create Notification model with migration file

php artisan make:model Notification -m

Add this to the migration file

Schema::create(‘notifications’, function (Blueprint $table) {
$table->id();
$table->foreignId(‘user_id’)->constrained()->onDelete(‘cascade’);
$table->string(‘title’); // Title of the notification
$table->text(‘body’); // Body content of the notification
$table->boolean(‘read’)->default(false); // Status to check if the notification has been read
$table->timestamps(); // Timestamps for created_at and updated_at
});

Now run the migration.

php artisan migrate

Now let us come to the model. Add fillable, cast relevant fields and add relationship to user table.

Please check that cast. It converts true/false values to save in db as 1/0. Cause db does not have a Boolean type. It saves 0 or 1.

<?php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentRelationsBelongsTo;

class Notification extends Model
{
use HasFactory;

/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
‘user_id’,
‘title’,
‘body’,
‘read’
];

/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
‘read’ => ‘boolean’,
];

/**
* Get the user that the notification belongs to.
*
* This method defines an inverse one-to-many relationship with the User model.
*
* @return IlluminateDatabaseEloquentRelationsBelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

This branch has changes for this
https://github.com/vimuths123/notification/tree/database_changes

Send Notification

In this section we are creating a small ui and implement functionality to save a notification in database. Here we give ability to choose the user to sent the notification.

First add the route.

Route::get(‘/send_notifications’, function () {
$users = User::all();
return Inertia::render(‘SendNotification’, [
‘users’ => $users
]);
});

Here we have get all the users and passing them to view for using on a dropdown. Here is the view.

<template>
<AppLayout title=”Send Notification”>
<div class=”container mx-auto p-4″>
<h1 class=”text-3xl font-bold text-gray-900″>Send a Notification</h1>
<form @submit.prevent=”createNotification” class=”mt-4″>
<div class=”mb-4″>
<label for=”user” class=”block text-sm font-medium text-gray-700″
>User</label
>
<select
id=”user”
v-model=”form.user_id”
class=”mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md”
>
<option disabled value=””>Please select a user</option>
<option v-for=”user in users” :key=”user.id” :value=”user.id”>
{{ user.name }}
</option>
</select>
</div>
<div class=”mb-4″>
<label for=”title” class=”block text-sm font-medium text-gray-700″
>Title</label
>
<input
type=”text”
id=”title”
v-model=”form.title”
class=”mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-500 focus:ring-opacity-50″
placeholder=”Notification title”
/>
</div>
<div class=”mb-6″>
<label for=”body” class=”block text-sm font-medium text-gray-700″
>Body</label
>
<textarea
id=”body”
v-model=”form.body”
rows=”3″
class=”mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-500 focus:ring-opacity-50″
placeholder=”Notification message”
></textarea>
</div>
<button
type=”submit”
class=”px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-bold rounded-md”
>
Send Notification
</button>
</form>
</div>
</AppLayout>
</template>

<script setup>
import AppLayout from “@/Layouts/AppLayout.vue”;
import { useForm } from “@inertiajs/vue3”;

const props = defineProps([“users”]);

const form = useForm({
user_id: “”,
title: “”,
body: “”,
});

const createNotification = () =>
form.post(route(“send_notifications”), {
preserveScroll: true,
onSuccess: () => form.reset(),
});
</script>

Here we are filling vue js form and sending data to backend. So let’s create backend functionality,

Route::post(‘/send_notifications’, function (Request $request) {
$notification = Notification::create([
‘user_id’ => $request->input(‘user_id’),
‘title’ => $request->input(‘title’),
‘body’ => $request->input(‘body’)
]);

return redirect()->back()->banner(‘Notification added.’);
})->name(‘send_notifications’);

Now you should be able to send a notification to db using UI. Here is the code up to this,

https://github.com/vimuths123/notification/tree/save_notification

Push the notification to Pusher

Now let’s push the notification to pusher. First you have to go to
https://pusher.com/ login create an app an get API keys. Then fill them in your .env file

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1

And don’t forget to change this to pusher

BROADCAST_DRIVER=pusher

Then lets create the event.

php artisan make:event NotificationCreated

This is our event. Here we have implements it from ShouldBroadcast and passed the notification object for broadcasting. Also we are broadcasting from a private channel for the time.

<?php

namespace AppEvents;

use IlluminateBroadcastingChannel;
use IlluminateBroadcastingInteractsWithSockets;
use IlluminateBroadcastingPresenceChannel;
use IlluminateBroadcastingPrivateChannel;
use IlluminateContractsBroadcastingShouldBroadcast;
use IlluminateFoundationEventsDispatchable;
use IlluminateQueueSerializesModels;

class NotificationCreated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;

public $notification;
/**
* Create a new event instance.
*/
public function __construct($notification)
{
$this->notification = $notification;
}

/**
* Get the channels the event should broadcast on.
*
* @return array<int, IlluminateBroadcastingChannel>
*/
public function broadcastOn(): array
{
return [
new Channel(‘notifications’),
];
}
}

Let’s call the event and pass the notification object now inside routes,

Route::post(‘/send_notifications’, function (Request $request) {
$notification = Notification::create([
‘user_id’ => $request->input(‘user_id’),
‘title’ => $request->input(‘title’),
‘body’ => $request->input(‘body’)
]);

event(new NotificationCreated($notification));

return redirect()->back()->banner(‘Notification added.’);
})->name(‘send_notifications’);

Then install pusher with this command

composer require pusher/pusher-php-server

Now after adding a notification when you goes to pusher dashboard you will see this

Listen to the notification

Echo
When an event is fired on the server, it’s broadcasted over a channel. Clients subscribed to that channel through Laravel Echo can listen for these events in real-time and take actions, like updating the UI immediately without a page refresh.

Now let us listen to the notification using echo. First we need to install it using npm

npm install laravel-echo pusher-js

Now go to this file, resourcesjsbootstrap.js And uncomment these lines

import Echo from ‘laravel-echo’;

import Pusher from ‘pusher-js’;
window.Pusher = Pusher;

window.Echo = new Echo({
broadcaster: ‘pusher’,
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? ‘mt1’,
wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? ‘https’) === ‘https’,
enabledTransports: [‘ws’, ‘wss’],
});

Then we need to add listen on our page. Add this to the bottom of script tag of resourcesjsLayoutsAppLayout.vue page,

const notificationCount = ref(0);
const notifications = ref([]);
const newNotification = ref({});

window.Echo.channel(“notifications”).listen(“NotificationCreated”, (e) => {
notificationCount.value++;
newNotification.value = {
id: e.notification.id,
title: e.notification.title,
body: e.notification.body
}
notifications.value.unshift(newNotification.value);
});

And change the dropdown like this,

<Dropdown align=”right” width=”96″>
<template #trigger>
<BellIcon class=”h-7 w-7 text-gray-600″ />

<span v-if=”notificationCount > 0″ class=”absolute bottom-3 left-3 flex items-center justify-center h-5 w-5 rounded-full bg-red-600 text-white text-xs”>
{{ notificationCount }}
</span>
</template>

<template #content>
<!– Account Management –>
<div class=”block px-4 py-2 text-xs text-gray-400 w-[350px]”>
Notifications
</div>

<div class=”border-t border-gray-200″ />

<div v-for=”(notification, index) in notifications” :key=”index”>
<DropdownLink :href=”route(‘dashboard’)”>
<div class=”block text-xs”>{{ notification.title }}</div>
<div>{{ notification.body }}</div>
</DropdownLink>

<div class=”border-t border-gray-200″ />
</div>
</template>
</Dropdown>

Now if you send a notification using form you will be able to see it receiving like this without refreshing.

Here is the git code until now.

Making notifications private

Now we are sending the notifications. But think about this. We are selecting a user to send a notification. But currently we are sending the notification to all users. We can prevent this and send the notification only to selected user. This is how to do it

First we create a separate channel for user. In appEventsNotificationCreated.php we change public function broadcastOn(): array to this

public function broadcastOn(): array
{
return [
new PrivateChannel(‘notifications.’.$this->notification->user_id),
];
}

Here we are sending the channel with user id at the end. And this is how we listen to it using echo in resourcesjsLayoutsAppLayout.vue.

import { usePage } from ‘@inertiajs/vue3’;

………..

window.Echo.private(“notifications.” + usePage().props.auth.user.id).listen(“NotificationCreated”, (e) => {
notificationCount.value++;
newNotification.value = {
id: e.notification.id,
title: e.notification.title,
body: e.notification.body
}
notifications.value.unshift(newNotification.value);
});

And you have to uncomment this line in this page configapp.php

AppProvidersBroadcastServiceProvider::class,

At the end we created two login users with our seeders. So login with admin@admin.com and admin2@admin.com from two different browsers and check notifications are coming only to needed user.

This is the git for this topic.
https://github.com/vimuths123/notification/tree/private_notifications

Now let us do some fine tunings,

Show pervious notifications and view notification functionality

First let us write backend code to get notifications

Route::get(‘get_notifications’, function (Request $request) {
return $user_notifications = Notification::where(‘user_id’, $request->user()->id)
->where(‘read’, false)
->latest()
->get();
})->name(‘get_notifications’);

Then let us take them and show inside front end.

import { onMounted, ref } from ‘vue’;

….

onMounted(() => {
axios.get(‘/get_notifications’)
.then(response => {
notifications.value = response.data;
notificationCount.value = response.data.length;
})
.catch(error => {
console.error(‘Error fetching notifications:’, error);
});
});

And now you will be able to see the notifications when you log in to the site.

First we write backend code

Route::get(‘click_notification/{notification}’, function (Notification $notification) {
$notification->read = true;
$notification->save();

return redirect()->back()->banner(‘Notification clicked.’);
})->name(‘click_notification’);

Here what you do is just change the flag and redirect back to the page. Remember in previous code we got only unread notifications. And now let’s change the front end. In resourcesjsLayoutsAppLayout.vue inside loop add this.

<DropdownLink :href=”route(‘click_notification’, notification.id)”>
<div class=”block text-xs”>{{ notification.title }}</div>
<div>{{ notification.body }}</div>
</DropdownLink>

Remember in real world don’t forget to add a nice page to read the notification.

This is the code up to now.
https://github.com/vimuths123/notification/tree/view_and_read_notifications

Optimise with Job queues and Redis

We can make this code asynchronous with jobs. This will make event asynchronous and save a lot of time.

this is how we do that.

Step 1: Configure the Queue Driver

First, update your .env file to use the database queue driver:

QUEUE_CONNECTION=database

This setting tells Laravel to use the database for queueing jobs.

Step 2: Create the Queue Table

Laravel needs a table in your database to store queued jobs. You can create this table by running the queue table migration that comes with Laravel. In your terminal, execute:

php artisan queue:table

Then, apply the migration to create the table:

php artisan migrate

And now we can use Redis for this jobs. This is how to do that.

Redis operates in memory, offering much faster read and write operations compared to disk-based systems. This is crucial for chat applications where timely processing of messages and notifications is critical for user experience.

By using Redis for queue management and temporary data storage (e.g., unread messages or active users), you can significantly reduce the load on your main database, reserving it for more critical tasks like persisting chat logs or user information.

Let’s check how to use redis for job queues

First install predis

composer require predis/predis

Then let’s tell laravel to use redis for queue. Add this in .env

REDIS_CLIENT=predis
QUEUE_CONNECTION=redis

And don’t forget to install redis on your system before

This is it. Enjoy the code.

Leave a Reply

Your email address will not be published. Required fields are marked *