Build a Real-Time Chat App with Laravel 11, Livewire 3 & Bootstrap 5

Build a Real-Time Chat App with Laravel 11, Livewire 3 & Bootstrap 5

In today’s fast-paced world, real-time communication is the heartbeat of modern applications. From social messaging platforms to customer support systems, live chat is everywhere—and users expect instant responsiveness.

In this tutorial, we'll walk through building a simple yet functional chat app that allows two users to exchange messages dynamically.

🛠️ Tools & Tech Stack

  • Laravel 11
  • Laravel Jetstream (Livewire or Inertia)
  • Livewire 3
  • Bootstrap 5
  • MySQL
  • Laravel Breeze (alternative to Jetstream)

✅ Step 1: Install Laravel Jetstream

Jetstream provides full-featured auth scaffolding: registration, login, email verification, two-factor authentication, profile management, and more.

🔧 Installation Steps:

composer require laravel/jetstream

Now, choose your stack:

For Livewire (recommended for Blade apps):

php artisan jetstream:install livewire

Then compile assets and run migrations:

npm install && npm run dev
php artisan migrate

📁 Jetstream Folder Structure Overview

  • resources/views/auth/ – Auth views (login, register, etc.)
  • resources/views/layouts/app.blade.php – App layout
  • resources/views/profile/ – Profile pages
  • resources/views/dashboard.blade.php – Dashboard
  • app/Actions/Fortify/ – Fortify actions
  • app/Actions/Jetstream/ – Jetstream team management
  • routes/web.php – Web routes
  • routes/auth.php – Auth routes

🧱 Step 2: Create the Messages Table

php artisan make:migration create_messages_table

Update your migration file:

Schema::create('messages', function (Blueprint $table) {
    $table->id();
    $table->foreignId('sender_id')->constrained('users')->onDelete('cascade');
    $table->foreignId('receiver_id')->constrained('users')->onDelete('cascade');
    $table->text('message');
    $table->boolean('is_read')->default(false);
    $table->timestamps();
});

Then run:

php artisan migrate

🧩 Step 3: Update the Layout with Bootstrap & Livewire

Update resources/views/layouts/app.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>{{ config('app.name', 'Laravel') }}</title>
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    @livewireStyles
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
        <div class="container">
            <a class="navbar-brand" href="{{ url('/') }}">{{ config('app.name', 'Laravel') }}</a>
            <div class="collapse navbar-collapse">
                <ul class="navbar-nav ms-auto">
                    @auth
                        <li class="nav-item"><span class="nav-link">Hi, {{ Auth::user()->name }}</span></li>
                        <li class="nav-item">
                            <form method="POST" action="{{ route('logout') }}">
                                @csrf
                                <button class="btn btn-link nav-link" type="submit">Logout</button>
                            </form>
                        </li>
                    @else
                        <li class="nav-item"><a class="nav-link" href="{{ route('login') }}">Login</a></li>
                        <li class="nav-item"><a class="nav-link" href="{{ route('register') }}">Register</a></li>
                    @endauth
                </ul>
            </div>
        </div>
    </nav>

    @isset($header)
        <header class="container mb-4"><div class="row"><div class="col">{{ $header }}</div></div></header>
    @endisset

    <main class="container">{{ $slot }}</main>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
    @livewireScripts
</body>
</html>

💬 Step 4: Create the Message Model

Create Message.php:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Message extends Model
{
    protected $fillable = ['sender_id', 'receiver_id', 'message'];
}

⚡ Step 5: Build the Livewire Chat Component

php artisan make:livewire Chat

Update app/Livewire/Chat.php:

namespace App\Livewire;

use Livewire\Component;
use App\Models\Message;
use Illuminate\Support\Facades\Auth;

class Chat extends Component
{
    public $receiverId;
    public $message;
    public $messages = [];

    protected $rules = ['message' => 'required|string'];

    public function mount($receiverId)
    {
        $this->receiverId = $receiverId;
        $this->loadMessages();
    }

    public function loadMessages()
    {
        $this->messages = Message::where(function ($q) {
            $q->where('sender_id', Auth::id())->where('receiver_id', $this->receiverId);
        })->orWhere(function ($q) {
            $q->where('sender_id', $this->receiverId)->where('receiver_id', Auth::id());
        })->orderBy('created_at')->get()->toArray();
    }

    public function sendMessage()
    {
        $this->validate();

        Message::create([
            'sender_id' => Auth::id(),
            'receiver_id' => $this->receiverId,
            'message' => $this->message
        ]);

        $this->message = '';
        $this->loadMessages();
    }

    public function render()
    {
        return view('livewire.chat');
    }
}

🖼️ Step 6: Create the Chat UI

resources/views/livewire/chat.blade.php

<div class="container mt-4">
    <div class="card">
        <div class="card-header bg-primary text-white">
            <h5 class="mb-0">Chat</h5>
        </div>
        <div class="card-body" style="height: 300px; overflow-y: auto;" wire:poll.2s="loadMessages">
            @foreach ($messages as $msg)
                <div class="mb-2 d-flex {{ $msg['sender_id'] == auth()->id() ? 'justify-content-end' : 'justify-content-start' }}">
                    <span class="badge p-2 {{ $msg['sender_id'] == auth()->id() ? 'bg-primary' : 'bg-secondary' }}">
                        {{ $msg['message'] }}
                    </span>
                </div>
            @endforeach
        </div>
        <div class="card-footer">
            <div class="input-group">
                <input type="text" wire:model="message" class="form-control" placeholder="Type a message..." required>
                <button wire:click="sendMessage" class="btn btn-primary">Send</button>
            </div>
        </div>
    </div>
</div>

🌐 Step 7: Add Route & Chat Controller

web.php:

use App\Http\Controllers\ChatController;

Route::get('/chat/{receiverId}', [ChatController::class, 'chat'])->middleware('auth');

ChatController.php:

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ChatController extends Controller
{
    public function chat()
    {
        return view('chat');
    }
}

resources/views/chat.blade.php

<x-app-layout>
    <livewire:chat :receiverId="request()->route('receiverId')" />
</x-app-layout>

✅ Done! Time to Chat

  • Log in as User A
  • Visit /chat/{userB_id}
  • http://127.0.0.1:8000/chat/{userB_id}
  • Start chatting 💬

Livewire’s wire:poll.2s gives your app a real-time-like experience, updating the messages every 2 seconds.

💡 Final Thoughts

You've just built a basic real-time chat app with Laravel 11 and Livewire 3—without writing a single line of JavaScript! 🚫💻

To take this even further, consider:

  • Laravel Echo + Pusher for true WebSocket-powered real-time chat
  • Typing indicators, message read receipts, and media attachments
  • Group chats or channel-based messaging.


0 Comments