Laravel Reverb is a real-time WebSocket framework that broadcasts events from Laravel to the frontend. It allows for real-time data synchronization across connected clients without page reloads. Vue is a JavaScript framework that allows for a reactive, component-based frontend experience.
In this guide, we’ll explore building a fully functional, real-time chat application using Laravel Reverb’s backend and Vue’s reactive frontend. As usual, the final, functioning code is available in this GitHub repository.
Before starting the development, you must set up an environment consisting of two components: the Laravel one, which is PHP-based, and the Vue/Node component.
php -v
to check the version)composer
to check that it exists) node -v
to check the version)In the following image, you can see the output of the command above on my Windows machine:
For the database, we will use SQLite, so be sure to activate it in your php.ini
file. Once you have met all the basic prerequisites, you can create a Laravel project by using the following:
> composer create-project laravel/laravel:^11.0 laravel_chat_demo
Once you get your project root dir ready to start the development, you might need to install some more requirements; this process depends on your specific environment but, in general, try to make your composer
command happy (🙂), if it complains (or just warns you) that a package is missing, install that package (Google is your friend).
Once everything is in place, you should be able to run the following:
> php artisan serve
This will fire up your development environment.
Now that the development environment is (hopefully) fine, we can concentrate on the stimulating part of the development.
We are writing a chat web application so we can expect to handle messages. With the following command, we will get a brand new class in the /app/Models
that will represent the messages exchanged in our chat:
> php artisan make:model -m Message
Right now, the class is empty. We will specialize it with data and functionalities, so replace the file Message.php
with the following code:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Message extends Model { use HasFactory; public $table = 'messages'; protected $fillable = ['id', 'user_id', 'text']; public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); } public function getTimeAttribute(): string { return date( "d M Y, H:i:s", strtotime($this->attributes['created_at']) ); } }
The code above describes a message in our system: messages will be stored in the table named messages
; each message comprises three fields: id
, user_id
, and text
. The rest of the code does two things: it creates a function that returns the user
associated with the message by using the BelongsTo
trait, and it defines a function that formats the field created_at
in a more human-readable date and time format. It will be used on the top of each message in the chat box.
The model we just wrote will instruct Eloquent (the Laravel ORM) on how to interact with data on our database, the next step is to actually create the table on the database; to do so we need a migration, that is, a fragment of code that will create the table:
> php artisan make:migration create_message_table
This will create an empty migration file in database\migrations\
whose name will be something like <date and time>_create_message_table.php
. The file contains two key methods: up
and down
. The up
method introduces changes to your database schema, such as creating new tables, adding columns, or establishing indexes.
Conversely, the down
method’s role is to undo or reverse the modifications made by the corresponding up
method, effectively allowing you to roll back changes if needed. We specialize this file with the following code:
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('messages', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->foreignId('user_id')->constrained(); $table->text('text')->nullable(); }); } public function down(): void { Schema::dropIfExists('messages'); } };
This migration defines a new messages table with some fields: an auto-incrementing ID as the primary key, a user_id
that references the users table to link messages with their senders, and a text field to store the message content.
The table also incorporates timestamp columns to track when each message is created and modified automatically; timestamps will create the two fields created_at
and updated_at
. As a precautionary measure, the operation includes a reversal method that can remove the messages table if needed, allowing for easy rollback of these changes.
At this point, let’s run the following:
> php artisan migrate:fresh
This will create a new table in the database. If you want to check that it was successful, simply open the database\database.sqlite
file.
Now that we have room for data, it is time to create the frontend. When developing the user interface for a Laravel application, you have two main options: the first involves using PHP to construct your frontend. In contrast, the second uses JavaScript frameworks like Vue or React.
We will use Vue in this article. This will also allow developers to take advantage of the huge packages ecosystem and tools available via npm. First, we need to install the appropriate package:
> composer require laravel/ui
Once the laravel/ui
package has been installed, you may install the frontend scaffolding using the artisan
command: the following command generates the UI for handling the registration and the login to our web app; just consider how complex this task can be if you should write this from scratch and how incredibly easy it is handled in Laravel:
> php artisan ui vue --auth
At this point, we have the two halves of the project in place: the PHP is already running with the PHP artisan serve command; now it is the moment to run the JavaScript-based part with:
> npm install
To have both the parts running, open two shell windows; one running the artisan
command, and the other with the following command:
> npm run dev
This command will keep the frontend running and reload it every time we modify it.
At the end of this process, without writing a single line of code, we have a fully functional website with the possibility of handling user authentication. Go to http://127.0.0.1:8000/register to register a new user and log in to the website:
Now, we need to add routes for the different APIs we are going to host:
/home
for the home page – this should already be present/message
, a POST HTTP method for adding a new message/messages
to GET all the existing messagesWe will modify the /routes/web.php
as follows:
<?php use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; use App\Http\Controllers\HomeController; Route::get('/', function () { return view('welcome'); }); Auth::routes(); Route::get('/home', [HomeController::class, 'index']) ->name('home'); Route::get('/messages', [HomeController::class, 'messages']) ->name('messages'); Route::post('/message', [HomeController::class, 'message']) ->name('message');
Now, it is time to write the HomeController
that will implement the APIs we described above:
<?php namespace App\Http\Controllers; use App\Jobs\SendMessage; use App\Models\Message; use App\Models\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; class HomeController extends Controller { public function __construct() { $this->middleware('auth'); } public function index() { $user = User::where('id', auth()->id())->select([ 'id', 'name', 'email', ])->first(); return view('home', [ 'user' => $user, ]); } public function messages(): JsonResponse { $messages = Message::with('user')->get()->append('time'); return response()->json($messages); } public function message(Request $request): JsonResponse { $message = Message::create([ 'user_id' => auth()->id(), 'text' => $request->get('text'), ]); SendMessage::dispatch($message); return response()->json([ 'success' => true, 'message' => "Message created and job dispatched.", ]); } }
Here you can see the logic behind the APIs described above:
/home
method, we retrieve the logged-in user’s information from the database using the User
model and pass it to the view/messages
method, we fetch all messages from the database via the Message
model, include the related user data, add the time
field (using an accessor) to each message
, and send the complete set to the view/message
method, we’ll create a new message
in the database using the Message
model and dispatch the SendMessage
queue jobWhen everything is set, we can install and configure Laravel events and queue jobs to host the exchange and synchronization of messages.
Laravel’s event and queue job systems provide powerful tools for handling asynchronous tasks and decoupling various parts of an application.
Events allow you to define and broadcast specific actions or changes within your application, which listeners can then respond to. Queue jobs enable you to offload time-consuming tasks, such as sending emails or processing large datasets, to background workers, ensuring that your application remains responsive and efficient.
Together, these features enhance scalability and improve the overall user experience by managing processes in the background. We will use both: the event
is essentially a data container that holds the message, and the QueueListener
handles the number of messages waiting to be dispatched.
With the following command, we generate the Event
class in the /app/Events
directory:
> php artisan make:event GotMessage
Then we have to implement two things: the constructor that will describe what is the payload of this event, and the broadcastOn()
method to specify on which channel these events will be broadcasted:
<?php namespace App\Events; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; class GotMessage implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; public function __construct() { } public function broadcastOn() { return new PrivateChannel("channel_for_everyone"); } }
The name of the channel (“channel_for_everyone
“) will be used in the file describing the WebSocket (see below) that will represent the communication channel between each instance of the chat client and the server. Note here that the constructor takes no parameters and there is no reference to the messages: the idea is that this event will be broadcasted once a new message has been sent, once the client receives it, they will just request the updated list of messages to the server using the services we implemented before.
To generate the QueueListener
, we use the following:
> php artisan make:job SendMessage
This command generates the SendMessage.php
file in the /app/Jobs
directory. This file instructs the Laravel framework on how to handle newly created instances of the GotMessage
event that we defined earlier:
<?php namespace App\Jobs; use App\Events\GotMessage; use App\Models\Message; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; class SendMessage implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct(public Message $message) { // } public function handle(): void { GotMessage::dispatch([ 'id' => $this->message->id, 'user_id' => $this->message->user_id, 'text' => $this->message->text, 'time' => $this->message->time, ]); } }
The handle()
method dispatches the GotMessage
event with details such as the message id
, user_id
, text
, and timestamp
. This job is designed to run in the background asynchronously processing events as long as they are enqueued on the channel, enabling efficient handling of message-sending tasks in the background. As you can see, there is a public $message
property as a constructor argument; this helps implement the queue process more efficiently (see the documentation for additional details)
With all other components in place, we now just need to add the final PHP element for the project: the WebSocket.
Laravel Reverb brings a real-time WebSocket framework that allows you to send events from your Laravel application to the frontend using WebSockets. With Reverb, it is possible to broadcast the events we defined above, reflected on each client connected, without requiring a page reload. As usual, adding this quite complex feature is addressed with one simple command:
> php artisan install:broadcasting
By accepting the default option, it will install both the PHP part for the backend and the Node dependencies to be used on the frontend.
Running this command will make several changes to your project directory: it will add a new section to the .env
file for Reverb, create a reverb.php
file in /config
to read these new fields, and, most importantly, add a channels.php
file in /routes
. In this channels.php
file, we’ll configure Reverb to create the channel_for_everyone
channel by adding the following code:
Broadcast::channel('channel_for_everyone', function ($user) { return true; });
At this point, everything is in place on the backend. Now we can focus on the frontend.
In this section, we will design a simple frontend, focusing on functionality and integration, without any styling, font customization, etc.
The first step is to set up the Vue environment:
> npm install vue vue-router @vitejs/plugin-vue
Now we can focus on three files to integrate a Vue template in Laravel. The first one is resources/js/app.js
, which will instantiate the Vue component that contains our app:
import './bootstrap'; import App from './App.vue' import { createApp } from 'vue'; const app = createApp({}); app.component('app', App); app.mount("#app");
The code is simple: App.vue
is our Vue template and we just associate it with the div
whose id
is #app
in the blade
file (see the next code snippet) that will render our web app. The next file is resources/js/App.vue
, which contains the template:
<script setup> const props = defineProps({ isAuth: { type: Boolean, default: false }, user: { type: [Object, Array], required: false } }) </script> <template> <h1>Hello, {{ user.name }}</h1> </template>
The code does two things: it defines the props
for Vue to host the values coming from Laravel, and then displays a simple message. As you can see, we just received the current authenticated user
and show their name
.
The last file is the blade
that will glue together the Vue component and the data we want to feed to it. Let’s just copy the following code in the resources\views\welcome.blade.php
file:
<!DOCTYPE html> <head> <title>Laravel + Vue Chat</title> @vite(['resources/js/app.js']) </head> <body> <div class="min-h-screen bg-gray-100" id="app"> <header > @if (Route::has('login')) <nav> @auth <div id="app"> <app :is-auth="{{ json_encode(auth()->check()) }}" :user="{{ auth()->check() ? auth()->user() : 'null' }}">> </app> { ... Logout code .. } </div> @else <a href="{{ route('login') }}"> Log in </a> @if (Route::has('register')) <a href="{{ route('register') }}"> Register </a> @endif @endauth </nav> @endif </header> </div> </body> </html>
In the code above, we first include the app.js
file to access the Vue component we just created. Then it renders the Login/Register
options if the user is not logged in or passes the is-auth
and user
variables to the props we defined in the resources/js/App.vue
file above.
In the chunk of code above, the Logout code is missing. Check the repository for additional details.
To verify that everything works, you will need to keep running four server components in four different shell windows:
> npm run build
> php artisan queue:listen
> php artisan reverb:start
> php artisan serve
Open your browser, and go to http://127.0.0.1:8000/; our brand-new chat app will appear. In the image below, you can see the four services running one beside the other:
If everything is in place, you can show in your browser the following message (keep in mind that the name will be different! Unless, of course, you sign in with the name Rosario 🙂):
Before finalizing the application, I’ll give you two useful tips if you incur problems during the development:
storage\logs\laravel.log
; this is where you can write logs with the Log
facility and where each component of your system will complain if something is not workingreverb:start
with php artisan reverb:start --debug
to have more debug messagesNow it is time to complete the frontend with all the required pieces of a chat web app: a text box of new messages and a list of messages:
<script> import axios from 'axios'; export default { data() { return { messages: [], newMessage: "", }; }, methods: { async postMessage(text) { try { await axios.post(`/message`, { text, }); // After posting, retrieve messages to include the new one this.getMessages(); } catch (err) { console.log(err.message); } }, async getMessages() { try { const response = await axios.get('/messages'); this.messages = response.data; // Scroll to the bottom after messages are updated this.scrollToBottom(); } catch (err) { console.log(err.message); } }, sendMessage() { if (this.newMessage.trim() !== "") { this.postMessage(this.newMessage.trim()); this.newMessage = ""; } else { return; } }, scrollToBottom() { this.$nextTick(() => { const messageList = document.getElementById('messagelist'); if (messageList) { messageList.scrollTop = messageList.scrollHeight; } }); } }, created() { this.getMessages(); window.Echo.private("channel_for_everyone") .listen('GotMessage', (e) => { this.getMessages(); }); }, }; </script> <template> <div class="container"> <div class="chat-box" id="messagelist"> <div v-for="(message, index) in messages" :key="index" class="message"> <strong>{{ message.user.name }}:</strong> {{ message.text }} <small class="text-muted float-right">{{ message.time }}</small> </div> </div> <div class="input-area"> <input v-model="newMessage" @keyup.enter="sendMessage" type="text" placeholder="Type your message here..." /> <button @click="sendMessage">Send</button> </div> </div> </template> <style scoped> .chat-box { border: 1px solid #ccc; padding: 10px; max-height: 300px; overflow-y: auto; } .message { margin-bottom: 10px; } </style>
The code, as you can see, is a bit longer but it is fairly simple:
data
method defines the two global objects we manipulate here, the messages array that contains the messages already sent to the chat and the newMessage
that contains the new text message sent by the userpostMessage
to do an HTTP POST to the /message
API we defined above to send a message text to the backendgetMessages
, which invokes the /getMessages
API to get the messages in the DBsendMessage
, which invokes the async method postMessage
and cleans the textbox once the message has been POSTed to the backendscrollToBottom
, which just scrolls the list of messages to the last message received by the backendcreated
is the method that runs when the component is first created. It updates the list of existing messages by invoking getMessage
and then instructs the Echo service (which listens on the WebSocket) to subscribe to new events on the channel_for_everyone
channel we defined aboveAfter the methods’ definition, you can see the template of the Vue component with very simple HTML for the message box the text box for new messages, and a little CSS to style everything.
The following image shows the final UI of the chat web app running in the browser:
In this project, we built a real-time chat app using Laravel Reverb and Vue, demonstrating how to integrate Laravel’s event-driven backend with Vue’s reactive frontend. Use the information learned here to develop scalable, interactive web applications. Explore the final code on GitHub to see Laravel Reverb and Vue in action.
Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps, including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error and what state the application was in when an issue occurred.
Modernize how you debug your Vue apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.