Build a nice Realtime notification with Laravel Jetstream (InertiaJS / Vue 3 stack).
Published: 2024-04-26 12:04:58

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.

###<u>Laravel Jetstream ​</u>

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 `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',
        ]);

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


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ap8wt0tagc2l9lo7a97.png)

### <u>Now let us build the UI</u>
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>
```

And don't forget to import BellIcon like this,

```
import { BellIcon } from '@heroicons/vue/24/solid'
```

Now you will see this in browser,


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fo58ylzioy2eov60rf04.png)

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 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);
    }
}
```

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

##<u>Send Notification</u>

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. this is the file path

`resources/js/Pages/SendNotification.vue`

```
<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

##<u>Push the notification to Pusher</u>
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 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'),
        ];
    }
}
```

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


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kww0enogm7pizklms5wp.png)

**<u>Listen to the notification</u>**

**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, `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'],
});
```

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);
});
```

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.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vhm9hwyg40yen0csqyk1.png)

Here is the git code until now. 

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

## <u>Making notifications private</u>
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),
        ];
    }
```

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);
});
```

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

```
App\Providers\BroadcastServiceProvider::class,
```

And in this file routes/channels.php

```
Broadcast::channel('notifications.{userId}', function ($user, $userId) {
    return (int) $user->id === (int) $userId;
});
```

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,

## <u>Show pervious notifications and view notification functionality</u>

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 `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>
```

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

## <u>Optimise with Job queues and Redis</u>
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
php artisan queue:work
```

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.