In this article we will implement a Real-time notifications using Laravel Reverb package and Vue.
Laravel Reverb is a first party Websocket for laravel apps that can be used to real-time messaging between client and server. Laravel Reverb used for different kinds of real-time apps like chat applications, real-time graphs, notifications, etc.
In this article we will learn about it, how to configure, and setting up server and client logic. We will implement a simple real time notifications that can be used in dashboards and admin panels to trigger for specific actions.
Make sure you have PHP 8.2 or later installed. Let’s start by creating new laravel 11 project:
composer create-project laravel/laravel laravel_reverb_notifications
Next open the project in your IDE, we will make some configurations:
First open .env and replace this line:
DB_CONNECTION=sqlite
With
DB_CONNECTION=mysql
As i am using mysql as the database. Then un-comment the db related variables, set the db username and password like so:
DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=laravel_reverb_notification DB_USERNAME=<db username> DB_PASSWORD=<db password>
you can also set the SESSION_DRIVER, CACHE_STORE to be file like so:
SESSION_DRIVER=file CACHE_STORE=file
Creating Migration
Let’s generate a migration and model for our notifications table:
php artisan make:model Notification -m
This command generate the Notifications model class and the “-m” option creates the migration.
Open the newly created migration file and update it like so:
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migrations. */ public function up(): void { Schema::create('notifications', function (Blueprint $table) { $table->id(); $table->text('content'); $table->string('icon')->nullable(); $table->tinyInteger('seen')->default(1); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('notifications'); } };
Next run the migrate command as shown:
php artisan migrate
You will be asked to create the database if not exists, just select Yes and continue.
Open the app/Models/Notification.php model add the $fillable property like so:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Notification extends Model { use HasFactory; protected $fillable = [ "content", "icon", "seen" ]; }
Installing Reverb
To use Reverb, we have to install laravel broadcasting:
php artisan install:broadcasting
Once running this command, the config/broadcasting.php config file will be generated
Then you will be asked to install Laravel Reverb, select “Yes”:
This will install Laravel Reverb composer package. After this step you will see that config/reverb.php config file generated.
Next you will be asked to install the Node dependencies required for broadcasting:
This step specifically run the “npm install” command internally and build the dependencies.
After installation successfully completes open your .env you will see a set of new env variables added:
REVERB_APP_ID=793430 REVERB_APP_KEY=<reverb app key> REVERB_APP_SECRET=<reverb app secret> REVERB_HOST="localhost" REVERB_PORT=8080 REVERB_SCHEME=http
These env variables used to configure reverb connection. The first three variables (REVERB_APP_ID, REVERB_APP_KEY, REVERB_APP_SECRET) represent the application credentials and must exchanged between the client and server.
The REVERB_HOST and REVERB_PORT instruct laravel reverb where send broadcast messages. The REVERB_SCHEME specifies whether you are using ssl for the REVERB_HOST.
Installing Tailwindcss
Install tailwindcss into the project:
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p
This will generate tailwind.config.js in the project root directory, open it and update the content section like shown:
content: [ "./resources/**/*.blade.php", "./resources/**/*.js", "./resources/**/*.vue", ]
Then include the tailwind directives in app.css
resources/css/app.css
@tailwind base; @tailwind components; @tailwind utilities;
Run the build process:
npm run dev
Installing Vuejs
Install vue 3 with npm:
npm install vue@latest
Next install vue vite plugin:
npm install --save-dev @vitejs/plugin-vue
Add the Vue config part to the vite.config.js
file:
import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import vue from "@vitejs/plugin-vue"; export default defineConfig({ plugins: [ laravel({ input: ['resources/css/app.css', 'resources/js/app.js'], refresh: true, }), vue() ], });
To check this is working run:
npm run dev
Creating Home view
Let’s create the blade view that will be our landing page to the view app
resources/views/home.blade.php
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Real time notifications</title> <!-- Fonts --> <link rel="preconnect" href="https://fonts.bunny.net"> <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" /> @vite(['resources/css/app.css', 'resources/js/app.js']) </head> <body class="font-sans antialiased dark:bg-black dark:text-white/50"> <div id="app"></div> </body> </html>
Most notably in this view i added the @vite directive to load the compiled css and js. And the div#app that will be the mount point for the app view.
Update routes/web.php to load this view:
Route::get('/', function () { return view('home'); });
Create the App component in resources/js/components
resources/js/components/App.vue
<script setup> import {ref} from "vue"; const showNotifications = ref(false); const toggleNotifications = () => { showNotifications.value = !showNotifications.value; } </script> <template> <header class="flex justify-between bg-gray-500 text-white px-3 pt-3 pb-3"> <div class="brand"> Real-time Notifications </div> <div class="notifications relative"> <a href="#" @click.prevent="toggleNotifications" title="notifications"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0M3.124 7.5A8.969 8.969 0 0 1 5.292 3m13.416 0a8.969 8.969 0 0 1 2.168 4.5" /> </svg> </a> <div class="dropdown absolute top-[37px] right-0 bg-blue-400 w-[230px] px-2 py-2 max-h-[260px] overflow-y-auto" :class="{'hidden': !showNotifications}"> <ul> <li class="mb-3 pb-1 border-b bg-gray-200 rounded text-black px-2"> <div> <span class="fa fa-info-circle pe-1"></span> <span class="text-sm">lorem ipsum lorem ipsum</span> </div> <time> <span class="fa fa-clock text-xs"></span> <span class="text-xs">Now</span> </time> </li> <li class="mb-3 pb-1 border-b bg-gray-200 rounded text-black px-2"> <div> <span class="fa fa-question-circle pe-1"></span> <span class="text-sm">lorem ipsum lorem ipsum</span> </div> <time> <span class="fa fa-clock text-xs"></span> <span class="text-xs">2024-09-13 04:20 pm</span> </time> </li> <li class="mb-2 pb-1 bg-gray-200 rounded text-black px-2"> <div> <span class="fa fa-user pe-1"></span> <span class="text-sm">lorem ipsum lorem ipsum</span> </div> <time> <span class="fa fa-clock text-xs"></span> <span class="text-xs">2024-09-5 12:00 am</span> </time> </li> </ul> </div> </div> </header> <div class="mx-4 my-5"> <h3>Real time notifications with Laravel Reverb</h3> <p>Watch for notifications</p> </div> </template>
In this code i have added a basic component html styled with tailwindcss classes. This component will display a header along with a notification icon at the top. When you click on the notification icon it will show a list of notifications.
Let’s initialize the vue app in the app.js file:
resources/js/app.js
import './bootstrap'; import { createApp } from "vue"; import App from "./components/App.vue"; createApp(App).mount("#app");
Now run again:
npm run dev
and launch the app:
php artisan serve
Go to the browser and inspect the app, you will see the notification icon.
Installing Vue Toast
To display the notification, we will install a beautiful vue package used for toast messages,
npm install --save vue3-toastify
Next include the toast and toast css at the top of the App.vue
import {ref, onMounted} from "vue"; import { toast } from 'vue3-toastify'; import 'vue3-toastify/dist/index.css';
We can check that it’s already working by adding a test code on the mounted hook like so:
<script setup> .... .... onMounted(() => { toast("This is toast message", { autoClose: 2000, type: "info", theme: "colored" }); }); </script>
If you see the notification shown in the top of browser window, then it’s working fine. Now remove the code inside of the onMounted() hook.
Generating Notification Event
To start sending and subscribing to events we need to create a laravel event:
php artisan make:event NotificationSent
The NotificationSent event will be created in app/Events directory, update it as shown:
app/Events/NotificationSent.php
<?php namespace App\Events; use App\Models\Notification; use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; class NotificationSent implements ShouldBroadcastNow { use Dispatchable, InteractsWithSockets, SerializesModels; /** * Create a new event instance. */ public function __construct(public Notification $notification) { // } /** * Get the channels the event should broadcast on. * * @return array<int, \Illuminate\Broadcasting\Channel> */ public function broadcastOn(): array { return [ new Channel('notifications_channel'), ]; } }
First the event must implement ShouldBroadcastNow interface so that the event sent immediately. Then i updated the constructor to pass the $notification argument. On the broadcastOn() method we tell laravel the channel name to send the event to which is “notifications_channel” .
To register the channel name “notifications_channel”, open routes/channels.php and add the channel route:
Broadcast::channel('notifications_channel', function () { return true; });
Listening for Notifications
I have updated the App.vue to include the logic for listening for notification events:
resources/js/components/App.vue
<script setup> import {ref, onMounted} from "vue"; import { toast } from 'vue3-toastify'; import 'vue3-toastify/dist/index.css'; import axios from "axios"; const showNotifications = ref(false); const notifications = ref([]); const toggleNotifications = () => { showNotifications.value = !showNotifications.value; } const getAllNotifications = () => { axios.get("/notifications").then(response => { if(response.data.data) { response.data.data.forEach(n => { notifications.value = [n, ...notifications.value]; }); } }); } const markNotificationAsRead = (id) => { axios.put(`/notifications/${id}`).then(); } onMounted(() => { getAllNotifications(); Echo.channel(`notifications_channel`) .listen("NotificationSent", (response) => { notifications.value = [{ ...response.notification }, ...notifications.value]; toast(response.notification.content, { autoClose: 3000, type: "info", theme: "colored" }); markNotificationAsRead(response.notification.id); }); }); </script> <template> <header class="flex justify-between bg-gray-500 text-white px-3 pt-3 pb-3"> <div class="brand"> Real-time Notifications </div> <div class="notifications relative"> <a href="#" @click.prevent="toggleNotifications" title="notifications"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0M3.124 7.5A8.969 8.969 0 0 1 5.292 3m13.416 0a8.969 8.969 0 0 1 2.168 4.5" /> </svg> </a> <div class="dropdown absolute top-[37px] right-0 bg-blue-400 w-[230px] px-2 py-2 max-h-[260px] overflow-y-auto" :class="{'hidden': !showNotifications}"> <ul v-if="notifications.length"> <li v-for="notification in notifications" :key="notification.id" class="mb-3 pb-1 border-b bg-gray-200 rounded text-black px-2"> <div> <span class="pe-1" :class="notification.icon"></span> <span class="text-sm">{{notification.content}}</span> </div> <time> <span class="fa fa-clock text-xs"></span> <span class="text-xs">{{notification.formatted_time}}</span> </time> </li> </ul> <ul v-else> <li>No notifications</li> </ul> </div> </div> </header> <div class="mx-4 my-5"> <h3>Real time notifications with Laravel Reverb</h3> <p>Watch for notifications</p> </div> </template>
On the onMounted() hook i am listening for notification events using laravel Echo.channel method:
Echo.channel(`notifications_channel`) .listen("NotificationSent", (response) => { })
The channel() method accepts the channel name argument we specified above. Then attaching the listen() method which accepts the event name and callback arguments. The event name is the class name “NotificationSent“.
The Echo object is configured inside of resources/js/echo.js file:
window.Echo = new Echo({ broadcaster: 'reverb', key: import.meta.env.VITE_REVERB_APP_KEY, ... ... });
As you see the broadcaster specified as “reverb” along with other config options like key, wsHost, wsPort and so on.
In the Echo.listen() callback argument i receive the response, using this response i appends the response object to the list notifications and then triggering the toast message and finally call function markNotificationAsRead() to make http request using axios to update notification to be seen:
toast(response.notification.content, { autoClose: 3000, type: "info", theme: "colored" }); markNotificationAsRead(response.notification.id);
Next in the component html i replaced the dummy content for notification list to loop over notifications ref variable:
<li v-for="notification in notifications" :key="notification.id" class="mb-3 pb-1 border-b bg-gray-200 rounded text-black px-2"> <div> <span class="pe-1" :class="notification.icon"></span> <span class="text-sm">{{notification.content}}</span> </div> <time> <span class="fa fa-clock text-xs"></span> <span class="text-xs">{{notification.formatted_time}}</span> </time> </li>
The getAllNotifications() called on app load to fetch all notifications
Create the NotificationsController
php artisan make:controller NotificationsController
Open app/Http/Controllers/NotificationsController.php and update as shown:
<?php namespace App\Http\Controllers; use App\Models\Notification; use Illuminate\Http\Request; class NotificationsController extends Controller { public function index() { $notifications = Notification::where("seen", 1)->get(); return response()->json(data: ['data' => $notifications]); } public function update(Notification $notification) { $notification->update(["seen" => 0]); return response()->json(['success' => true]); } }
Add the routes for these two actions in routes/web.php:
Route::get('notifications', [\App\Http\Controllers\NotificationsController::class, 'index']); Route::put('notifications/{notification}', [\App\Http\Controllers\NotificationsController::class, 'update']);
Also update Notification model to include getFormattedTimeAttribute():
protected $appends = ['formatted_time']; protected function getFormattedTimeAttribute() { return $this->created_at->diffForHumans(); }
This snippet includes the “formatted_time” property along with the notification response object used in template above.
Emitting Notification Events
So far we added the logic to listen for events, now we need a way to send the notification. In real world application, this is coming from frontend app. But for simplicity i will make a a console command to send notification from:
php artisan make:command SendNotificationCommand
app/Console/Commands/SendNotificationCommand.php
<?php namespace App\Console\Commands; use App\Events\NotificationSent; use App\Models\Notification; use Illuminate\Console\Command; class SendNotificationCommand extends Command { protected $signature = 'app:send-notification-command {text} {type}'; protected $description = 'Command description'; /** * Execute the console command. */ public function handle() { $icon = match($this->argument("type")) { 'user' => "fa fa-user", 'question' => "fa fa-question-circle", default => "fa fa-info-circle" }; $notification = Notification::create([ "content" => $this->argument("text"), "icon" => $icon ]); broadcast(new NotificationSent($notification)); } }
The command signature accepts two arguments, text (notification content) and type (notification icon). In the handle() method we check for the type argument to display the suitable icon css class using match expression.
Then we create the notification to be stored in the database. Finally we emit the event using laravel broadcast() function passing in an instance of NotificationSent object.
To check this run these commands in different terminals:
npm run dev
php artisan serve
And also launch the reverb server:
php artisan reverb:start
Open the browser and in other terminal window send notification command:
php artisan app:send-notification-command 'new user registration' user
php artisan app:send-notification-command 'new client inquiry' question
If everything is working fine you will see the notification like this: