DEV Community

vimuth
vimuth

Posted on

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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 database\seeders\DatabaseSeeder.php and add these code. This will seed two users (admin@admin.com and admin2@admin.com) in users table.

        \App\Models\User::factory()->create([
            'name' => 'admin',
            'email' => 'admin@admin.com',
        ]);

        \App\Models\User::factory()->create([
            'name' => 'admin2',
            'email' => 'admin2@admin.com',
        ]);

Enter fullscreen mode Exit fullscreen mode

Now run this command

php artisan migrate:fresh --seed
Enter fullscreen mode Exit fullscreen mode

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.

Image description

Now let us build the UI

For UI we are build a notification icon with notification count in the menu. We use 'resources\js\Layouts\AppLayout.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>
Enter fullscreen mode Exit fullscreen mode

And don't forget to import BellIcon like this,

import { BellIcon } from '@heroicons/vue/24/solid'
Enter fullscreen mode Exit fullscreen mode

Now you will see this in browser,

Image description

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
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

Now run the migration.

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

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 App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

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 \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And don't forget to change this to pusher

BROADCAST_DRIVER=pusher
Enter fullscreen mode Exit fullscreen mode

Then lets create the event.

php artisan make:event NotificationCreated
Enter fullscreen mode Exit fullscreen mode

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 App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

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, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn(): array
    {
        return [
            new Channel('notifications'),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

Then install pusher with this command

composer require pusher/pusher-php-server
Enter fullscreen mode Exit fullscreen mode

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

Image description

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
Enter fullscreen mode Exit fullscreen mode

Now go to this file, resources\js\bootstrap.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'],
});
Enter fullscreen mode Exit fullscreen mode

Then we need to add listen on our page. Add this to the bottom of script tag of resources\js\Layouts\AppLayout.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);
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

Image description

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 app\Events\NotificationCreated.php we change public function broadcastOn(): array to this

public function broadcastOn(): array
    {
        return [
            new PrivateChannel('notifications.'.$this->notification->user_id),
        ];
    }
Enter fullscreen mode Exit fullscreen mode

Here we are sending the channel with user id at the end. And this is how we listen to it using echo in resources\js\Layouts\AppLayout.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);
});
Enter fullscreen mode Exit fullscreen mode

And you have to uncomment this line in this page config\app.php

App\Providers\BroadcastServiceProvider::class,
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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);
        });
});
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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 resources\js\Layouts\AppLayout.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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then, apply the migration to create the table:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

REDIS_CLIENT=predis
QUEUE_CONNECTION=redis
Enter fullscreen mode Exit fullscreen mode

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

This is it. Enjoy the code.

Top comments (0)