Building a Real-Time Chat System with Laravel Reverb and Vue.js
Introduction
Real-time communication has become essential in modern web applications. Whether you're building a customer support system, team collaboration tool, or social platform, implementing chat functionality can significantly enhance user experience. With Laravel Reverb's recent introduction as Laravel's first-party WebSocket server, building real-time features has never been more straightforward.
In this guide, we'll build a complete real-time chat system using Laravel Reverb for WebSocket connections and Vue.js for the frontend. We'll cover authentication, message persistence, typing indicators, and online user presence.
Setting Up Laravel with Reverb
First, let's create a new Laravel project and install the necessary dependencies:
composer create-project laravel/laravel chat-app
cd chat-app
composer require laravel/reverb pusher/pusher-php-serverInstall Reverb and publish its configuration:
php artisan reverb:installThis command will install Reverb, publish the configuration file, and add the necessary environment variables to your .env file. Update your broadcasting configuration in config/broadcasting.php:
// config/broadcasting.php
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST', '127.0.0.1'),
'port' => env('REVERB_PORT', 8080),
'scheme' => env('REVERB_SCHEME', 'http'),
],
],Database Schema and Models
Let's create the necessary database tables for our chat system:
php artisan make:model Chat -m
php artisan make:model Message -mUpdate the migration files:
// database/migrations/create_chats_table.php
Schema::create('chats', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->boolean('is_private')->default(false);
$table->timestamps();
});
// database/migrations/create_messages_table.php
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->foreignId('chat_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->text('content');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});Create a pivot table for chat participants:
php artisan make:migration create_chat_user_table
// database/migrations/create_chat_user_table.php
Schema::create('chat_user', function (Blueprint $table) {
$table->id();
$table->foreignId('chat_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamp('joined_at')->useCurrent();
$table->timestamp('last_read_at')->nullable();
$table->unique(['chat_id', 'user_id']);
});Model Relationships
Define the relationships in your models:
// app/Models/Chat.php
class Chat extends Model
{
protected $fillable = ['name', 'description', 'is_private'];
public function messages()
{
return $this->hasMany(Message::class)->latest();
}
public function users()
{
return $this->belongsToMany(User::class)
->withPivot('joined_at', 'last_read_at')
->withTimestamps();
}
public function latestMessage()
{
return $this->hasOne(Message::class)->latestOfMany();
}
}
// app/Models/Message.php
class Message extends Model
{
protected $fillable = ['chat_id', 'user_id', 'content', 'read_at'];
protected $casts = ['read_at' => 'datetime'];
public function chat()
{
return $this->belongsTo(Chat::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
// Add to app/Models/User.php
public function chats()
{
return $this->belongsToMany(Chat::class)
->withPivot('joined_at', 'last_read_at')
->withTimestamps();
}
public function messages()
{
return $this->hasMany(Message::class);
}Creating Events and Broadcasting
Create events for real-time broadcasting:
php artisan make:event MessageSent
php artisan make:event UserTyping
php artisan make:event UserStoppedTypingImplement the MessageSent event:
// app/Events/MessageSent.php
class MessageSent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
public function __construct(Message $message)
{
$this->message = $message->load('user');
}
public function broadcastOn()
{
return new PrivateChannel('chat.' . $this->message->chat_id);
}
public function broadcastWith()
{
return [
'id' => $this->message->id,
'content' => $this->message->content,
'created_at' => $this->message->created_at,
'user' => [
'id' => $this->message->user->id,
'name' => $this->message->user->name,
],
];
}
}Implement typing events:
// app/Events/UserTyping.php
class UserTyping implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $user;
public $chatId;
public function __construct(User $user, $chatId)
{
$this->user = $user;
$this->chatId = $chatId;
}
public function broadcastOn()
{
return new PrivateChannel('chat.' . $this->chatId);
}
public function broadcastWith()
{
return [
'user' => [
'id' => $this->user->id,
'name' => $this->user->name,
],
];
}
}API Controllers
Create controllers to handle chat operations:
php artisan make:controller Api/ChatController
php artisan make:controller Api/MessageControllerImplement the controllers:
// app/Http/Controllers/Api/MessageController.php
class MessageController extends Controller
{
public function store(Request $request, Chat $chat)
{
$request->validate([
'content' => 'required|string|max:1000',
]);
// Check if user is participant
if (!$chat->users()->where('user_id', auth()->id())->exists()) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$message = $chat->messages()->create([
'user_id' => auth()->id(),
'content' => $request->content,
]);
$message->load('user');
broadcast(new MessageSent($message))->toOthers();
return response()->json($message, 201);
}
public function typing(Request $request, Chat $chat)
{
broadcast(new UserTyping(auth()->user(), $chat->id))->toOthers();
return response()->json(['status' => 'success']);
}
public function stoppedTyping(Request $request, Chat $chat)
{
broadcast(new UserStoppedTyping(auth()->user(), $chat->id))->toOthers();
return response()->json(['status' => 'success']);
}
}Vue.js Frontend Implementation
Install the necessary frontend dependencies:
npm install laravel-echo pusher-js axiosConfigure Echo in your JavaScript bootstrap file:
// resources/js/bootstrap.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT,
wssPort: import.meta.env.VITE_REVERB_PORT,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
authEndpoint: '/broadcasting/auth',
auth: {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
},
});Create a Vue.js chat component:
// resources/js/components/ChatRoom.vue
{{ typingText }}
Starting the Reverb Server
To run your real-time chat system, start the Reverb server:
php artisan reverb:startIn another terminal, start your Laravel application:
php artisan serveMake sure to run your queue worker for broadcasting: