Building a Real-Time Chat App with Laravel Reverb and Nuxt 3

Harish Kumar · · 2936 Views

Building a real-time chat application is a great way to understand the power of WebSockets and real-time communication. In this tutorial, we will walk through creating a Real-Time Chat Application using Laravel Reverb, Laravel Sanctum for SPA authentication, and Nuxt 3 for the frontend. We will cover everything from installing Reverb, setting up authentication, to broadcasting messages in real-time between users.

If you're new to using Laravel Sanctum for SPA authentication, check out this full tutorial: Nuxt 3 Authentication with Laravel Sanctum.

Let's dive into building the real-time chat application!

Want to Support My Work?

If you found this tutorial helpful and would like to support my work, feel free to check out some of my products:

  1. Spec Coder AI: A smart VSCode extension to help you code faster and more efficiently.

  2. Ctrl+Alt+Cheat: A cheat sheet extension for various programming languages and tools.

  3. JavaScript Ebook: Covering everything from ES2015 to ES2023.

Step 1: Install Laravel Reverb

In your Laravel application, the first step is to install Laravel Reverb, a WebSocket server that simplifies the setup for real-time broadcasting. To install Reverb, run the following artisan command:

php artisan install:broadcasting

This command installs the required dependencies, configures your broadcasting.php file, and publishes the necessary configuration files.

Step 2: Configure Reverb Environment Variables

Next, configure your .env file with the following Reverb environment variables:

REVERB_APP_ID=my-app-id
REVERB_APP_KEY=my-app-key
REVERB_APP_SECRET=my-app-secret
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http

These variables will be used by Reverb to handle WebSocket communication between your backend and frontend.

Step 3: Create a Chat Message Model and Migration

Next, create the model and migration for chat messages. In your terminal, run the following command to generate the migration:

php artisan make:model ChatMessage -m

Update the migration file to create the chat_messages table:

Schema::create('chat_messages', function (Blueprint $table) {
    $table->id();
    $table->foreignId('receiver_id');
    $table->foreignId('sender_id');
    $table->text('text');
    $table->timestamps();
});

Run the migration:

php artisan migrate

Step 4: Create a MessageSent Event

Next, create a new event that will broadcast when a message is sent between users. Run the following command:

php artisan make:event MessageSent

Update the MessageSent.php event class as follows:

<?php

namespace App\Events;

use App\Models\ChatMessage;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MessageSent implements ShouldBroadcastNow
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(public ChatMessage $message) {}

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel("chat.{$this->message->receiver_id}"),
        ];
    }
}

This event will broadcast messages to a private channel based on the receiver's ID.

Step 5: Define Broadcasting Channels

Open routes/channels.php and define a private channel for the chat functionality:

use Illuminate\Support\Facades\Broadcast;

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

This ensures that only authenticated users can listen to their respective private channels.

Step 6: Create Routes to Fetch and Send Messages

In your routes/api.php file, define two routes: one to fetch chat messages between users, and another to send messages.

Route::get('/messages/{user}', function (User $user, Request $request) {
    return ChatMessage::query()
        ->where(function ($query) use ($user, $request) {
            $query->where('sender_id', $request->user()->id)
                  ->where('receiver_id', $user->id);
        })
        ->orWhere(function ($query) use ($user, $request) {
            $query->where('sender_id', $user->id)
                  ->where('receiver_id', $request->user()->id);
        })
        ->with(['sender', 'receiver'])
        ->orderBy('id', 'asc')
        ->get();
})->middleware('auth:sanctum');

Route::post('/messages/{user}', function (User $user, Request $request) {
    $request->validate(['message' => 'required|string']);

    $message = ChatMessage::create([
        'sender_id' => $request->user()->id,
        'receiver_id' => $user->id,
        'text' => $request->message
    ]);

    broadcast(new MessageSent($message));

    return $message;
});

These routes will handle the fetching and sending of messages between users.

Step 7: Add Reverb Variables in Nuxt 3

In your Nuxt 3 app, add the following Reverb environment variables in your .env file:

NUXT_PUBLIC_REVERB_APP_ID=my-app-id
NUXT_PUBLIC_REVERB_APP_KEY=my-app-key
NUXT_PUBLIC_REVERB_APP_SECRET=my-app-secret
NUXT_PUBLIC_REVERB_HOST="localhost"
NUXT_PUBLIC_REVERB_PORT=8080
NUXT_PUBLIC_REVERB_SCHEME=http

Then, add these variables to your nuxt.config.ts:

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      REVERB_APP_ID: process.env.NUXT_PUBLIC_REVERB_APP_ID,
      REVERB_APP_KEY: process.env.NUXT_PUBLIC_REVERB_APP_KEY,
      REVERB_APP_SECRET: process.env.NUXT_PUBLIC_REVERB_APP_SECRET,
      REVERB_HOST: process.env.NUXT_PUBLIC_REVERB_HOST,
      REVERB_PORT: process.env.NUXT_PUBLIC_REVERB_PORT,
      REVERB_SCHEME: process.env.NUXT_PUBLIC_REVERB_SCHEME,
    },
  },
});

Step 8: Create laravel-echo.client.ts Plugin in Nuxt 3

To enable real-time broadcasting in Nuxt 3, we need to configure Laravel Echo to listen for WebSocket events. Create a new plugin called laravel-echo.client.ts in the plugins directory of your Nuxt 3 app.

import Echo from "laravel-echo";
import Pusher, { type ChannelAuthorizationCallback } from "pusher-js";

declare global {
  interface Window {
    Echo: Echo;
    Pusher: typeof Pusher;
  }
}

export default defineNuxtPlugin((_nuxtApp) => {
  window.Pusher = Pusher;

  const config = useRuntimeConfig();

  const echo = new Echo({
    broadcaster: "reverb",
    key: config.public.REVERB_APP_KEY,
    wsHost: config.public.REVERB_HOST,
    wsPort: config.public.REVERB_PORT ?? 80,
    wssPort: config.public.REVERB_PORT ?? 443,
    forceTLS: (config.public.REVERB_SCHEME ?? "https") === "https",
    enabledTransports: ["ws", "wss"],

    authorizer: (channel: any, options: any) => {
      return {
        authorize: (
          socketId: string,
          callback: ChannelAuthorizationCallback
        ) => {
          useSanctumFetch("api/broadcasting/auth", {
            method: "post",
            body: {
              socket_id: socketId,
              channel_name: channel.name,
            },
          })
            .then((response) => {
              callback(null, response);
            })
            .catch((error: Error) => callback(error, null));
        },
      };
    },
  });

  return {
    provide: { echo },
  };
});

This plugin configures Laravel Echo to use the Reverb WebSocket server and authorizes users with Laravel Sanctum when broadcasting events.

Step 9: Implement Real-Time Chat in Nuxt 3

To enable the real-time chat feature in our Nuxt 3 app, I created a new page, chats > [id].vue, which allows users to chat with each other by selecting a user. This page is connected to a Laravel backend that handles the chat messages and user data, while Laravel Echo and websockets are used for real-time updates and typing indicators.

Here's the breakdown of how I structured this chat functionality.

Load Selected User's Chat Data

First, we retrieve the userID from the URL using the useRoute composable. This userID is used to identify the user we are chatting with and load the necessary user and chat messages.

const route = useRoute();
const userID = route.params.id;
const { user: currentUser } = useSanctum<User>();

const { data: user } = await useAsyncData(
    `user-${userID}`, () => useSanctumFetch<User>(`/api/users/${userID}`)
);

const { data: messages } = useAsyncData(
    `messages-${userID}`,
    () => useSanctumFetch<ChatMessage[]>(`/api/messages/${userID}`),
    {
        default: (): ChatMessage[] => []
    }
);

This snippet fetches the current chat messages for the selected user using useSanctumFetch, a custom composable for handling authentication in our Nuxt app. We then use useAsyncData to load the messages asynchronously when the page is mounted.

Displaying Chat Messages

Once we have the messages, we display them in a scrollable container. Each message is styled differently based on whether the message is from the current user or the other participant in the chat. We use a v-for loop to render each message dynamically.

<div ref="messagesContainer" class="p-4 overflow-y-auto max-h-fit">
    <div v-for="message in messages" :key="message.id" class="flex items-center mb-2">
        <div v-if="message.sender_id === currentUser.id"
            class="p-2 ml-auto text-white bg-blue-500 rounded-lg">
            {{ message.text }}
        </div>
        <div v-else class="p-2 mr-auto bg-gray-200 rounded-lg">
            {{ message.text }}
        </div>
    </div>
</div>

We also ensure that the chat window scrolls to the latest message automatically after a new message is sent or received by using nextTick().

watch(
    messages,
    () => {
        nextTick(() => messageContainerScrollToBottom());
    },
    { deep: true }
);

function messageContainerScrollToBottom() {
    if (!messagesContainer.value) {
        return;
    }

    messagesContainer.value.scrollTo({
        top: messagesContainer.value.scrollHeight,
        behavior: 'smooth'
    });
}

Sending New Messages

The user can type and send new messages using an input field. When a new message is submitted, we post it to the Laravel backend using a POST request.

const newMessage = ref("");

const sendMessage = async () => {
    if (!newMessage.value.trim()) {
        return;
    }

    const messageResponse = await useSanctumFetch<ChatMessage>(`/api/messages/${userID}`, {
        method: "post",
        body: { message: newMessage.value }
    });

    messages.value.push(messageResponse);
    newMessage.value = "";
};

The message is sent when the user presses the "Enter" key or clicks the send button, and the message list is updated immediately to include the new message.

Real-Time Features with Laravel Echo

To enable real-time updates, we use Laravel Echo to listen for two key events:

  1. MessageSent: to receive new messages from the other user.

  2. typing: to show typing indicators when the other user is typing a message.

onMounted(() => {
    if (currentUser.value) {
        $echo.private(`chat.${currentUser.value.id}`)
            .listen('MessageSent', (response: { message: ChatMessage }) => {
                messages.value.push(response.message);
            })
            .listenForWhisper("typing", (response: { userID: number }) => {
                isUserTyping.value = response.userID === user.value?.id;

                if (isUserTypingTimer.value) {
                    clearTimeout(isUserTypingTimer.value);
                }

                isUserTypingTimer.value = setTimeout(() => {
                    isUserTyping.value = false;
                }, 1000);
            });
    }
});

The MessageSent event updates the message list with real-time messages. The typing event shows a "user is typing" indicator, which disappears after 1 second of inactivity.

Typing Indicator

The typing indicator is triggered when a user starts typing a message. We send a "typing" event to the server via Laravel Echo’s whisper method.

const sendTypingEvent = () => {
    if (!user.value || !currentUser.value) {
        return;
    }

    $echo.private(`chat.${user.value.id}`).whisper("typing", {
        userID: currentUser.value.id
    });
};

The event is sent whenever the user types in the input field, and the recipient receives the event to display the typing indicator.

User Interface

The chat interface includes a header with the selected user's name and a main section for messages and the input field.

<template>
    <div>
        <header v-if="user" class="bg-white shadow">
            <div class="px-4 py-6 mx-auto max-w-7xl sm:px-6 lg:px-8">
                <h1 class="text-2xl font-bold ">{{ user.name }}</h1>
            </div>
        </header>

        <div class="flex flex-col items-center py-5">
            <div class="container w-full h-full p-8 space-y-3 bg-white rounded-xl">
                <!-- Chat Messages -->
                <div v-if="currentUser">
                    <div class="flex flex-col justify-end h-80">
                        <div ref="messagesContainer" class="p-4 overflow-y-auto max-h-fit">
                            <div v-for="message in messages" :key="message.id" class="flex items-center mb-2">
                                <div v-if="message.sender_id === currentUser.id"
                                    class="p-2 ml-auto text-white bg-blue-500 rounded-lg">
                                    {{ message.text }}
                                </div>
                                <div v-else class="p-2 mr-auto bg-gray-200 rounded-lg">
                                    {{ message.text }}
                                </div>
                            </div>
                        </div>
                    </div>

                    <div class="flex-shrink-0">
                        <span v-if="user && isUserTyping" class="text-gray-500">
                            {{ user.name }} is typing...
                        </span>
                        <div class="flex items-center justify-between w-full p-4 border-t border-gray-200">
                            <input type="text" v-model="newMessage" @keydown="sendTypingEvent"
                                @keyup.enter="sendMessage"
                                class="w-full p-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
                                placeholder="Type a message..." />
                            <button @click.prevent="sendMessage"
                                class="inline-flex items-center justify-center w-12 h-12 ml-4 text-white bg-blue-500 rounded-lg hover:bg-blue-600">
                                <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" viewBox="0 0 24 24"
                                    stroke="currentColor">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                                        d="M14 5l7 7m0 0l-7 7m7-7H3" />
                                </svg>
                            </button>
                        </div>
                    </div>
                </div>
                <!-- /Chat Messages -->

            </div>
        </div>
    </div>
</template>

This completes the real-time chat implementation using Laravel Sanctum and Echo with Nuxt 3.

Conclusion

In this tutorial, we successfully implemented a real-time chat application using Laravel Reverb, Laravel Sanctum, and Nuxt 3. We covered setting up the backend with Laravel to handle authentication, message storage, and event broadcasting using Laravel Reverb. On the frontend, we used Nuxt 3 to create a dynamic chat interface that listens for real-time messages and typing events using Laravel Echo and websockets.

By following these steps, you now have a solid foundation for building real-time applications that involve user interactions in real-time. You can expand this project to include additional features like group chats, message notifications, or even emojis and media file support.

For more details and the full source code of this project, feel free to check out the GitHub repository: Nuxt3-Laravel-Reverb-Chat.

If you want to learn more about SPA authentication with Laravel Sanctum and Nuxt 3, check out our full tutorial here: Set Up Nuxt 3 Authentication with Laravel Sanctum.

Happy coding!

Want to Support My Work?

If you found this tutorial helpful and would like to support my work, feel free to check out some of my products:

  1. Spec Coder AI: A smart VSCode extension to help you code faster and more efficiently.

  2. Ctrl+Alt+Cheat: A cheat sheet extension for various programming languages and tools.

  3. JavaScript Ebook: Covering everything from ES2015 to ES2023.

0

Please login or create new account to add your comment.

0 comments
You may also like:

15 Must-Know TypeScript Features to Level Up Your Development Skills

TypeScript has become the go-to tool for developers building scalable, maintainable JavaScript applications. Its advanced features go far beyond basic typing, giving developers (...)
Harish Kumar

JavaScript Best Practices: Tips for Writing Clean and Maintainable Code

JavaScript is one of the most versatile and widely used programming languages today, powering everything from simple scripts to complex web applications. As the language continues (...)
Harish Kumar

Ditch jQuery: Vanilla JS Alternatives You Need to Know

jQuery revolutionized web development by simplifying DOM manipulation, event handling, and animations. However, modern JavaScript (ES6 and beyond) now provides many built-in methods (...)
Harish Kumar

Shallow Copy vs Deep Copy in JavaScript: Key Differences Explained

When working with objects and arrays in JavaScript, it's crucial to understand the difference between shallow copy and deep copy. These concepts dictate how data is duplicated (...)
Harish Kumar

A Beginner’s Guide to Efficient Memory Use in JavaScript

Managing memory efficiently in JavaScript applications is essential for smooth performance, especially for large-scale or complex applications. Poor memory handling can lead to (...)
Harish Kumar

Exploring the New Features of JavaScript ES2024: A Look into the Future of Web Development

Discovering new functionality in programming languages is a bit like a holiday — it’s filled with anticipation and the excitement of exploring something new. With the proposed (...)
Harish Kumar