category: How toApr 05, 2024

The Ultimate Guide to Laravel Reverb

Learn how to use Laravel Reverb to develop a real-time Laravel app. You'll learn about channels, events, broadcasting, authorization and configuring Laravel Reverb WebSocket Server connections.

Novu + Laravel Reverb

Laravel Reverb is a first-party WebSocket server for Laravel applications, providing real-time communication capabilities between the client and server seamlessly.

Laravel Reverb has numerous appealing features, including:

  • Speed and scalability.
  • Support for thousands of simultaneous connections.
  • Integration with existing Laravel broadcasting features.
  • Compatibility with Laravel Echo.
  • First-class integration and deployment with Laravel Forge.

In this guide, I’ll demonstrate how to use Laravel Reverb to develop a real-time Laravel app. You’ll learn about channels, events, broadcasting, and how to use Laravel Reverb to create quick and real-time applications in Laravel.

Furthermore, you’ll learn how to add real-time notifications to your Laravel Reverb app!

If you’re eager to explore the code immediately, you can view the completed code on GitHub. Let’s get started!

Install a Fresh Laravel App

Go ahead and create a new laravel app.

1laravel new carryon

I love to start new Laravel apps with one of the starter kits that ships with login, registration, email verification, etc. In this guide, I’ll use Laravel JetStream with Livewire.

You can follow the Laravel console prompt to ensure everything is set up properly with the right starter kit.

Run your migrations to set up the database with the users, jobs, cache & access tokens table.

1php artisan migrate
2

Now, run your app with php artisan serve. For folks with Herd or Valet, your app should already be available on http://carryon.test

You should have something like this:- A fresh new Laravel app with JetStream enabled. So beautiful! 🎉

Install Laravel Reverb

Now, we need to install Laravel Reverb – our WebSocket Server into the Laravel app.

Run the following command in your console and choose the Yes option for any of the prompts that show up:

1php artisan install:broadcasting

This command will do the following:

  • Publish the broadcasting config and channels route file.
  • Install Laravel Reverb (WebSocket server)
  • Install and build the Node dependencies required.

Next, open two new terminals to start up the reverb server and also run the client side.

First terminal: Start and run reverb server

1php artisan reverb:start
2

The reverb server is usually run on the 8080 port by default. You can see that in your console. If you need to specify a custom host or port, you may do so via the --host and --port options when starting the server like so:

1php artisan reverb:start --host=127.0.0.1 --port=9000
2

Second terminal: Run Vite to ensure any changes on the client is hot reloaded & instant.

1npm run dev
2

Check your .env file. You will notice a few additions to it. It added the credentials for running Reverb. And for the frontend to connect with the Reverb server.

The BROADCAST_CONNECTION has been set to use reverb. Alternative broadcast drivers are pusher, ably and log.

1BROADCAST_CONNECTION=reverb
2...
3
4REVERB_APP_ID=872050
5REVERB_APP_KEY=ed5zsi5ebpdmawcqbwva
6REVERB_APP_SECRET=zbttdgtacvuhfdo3dl0o
7REVERB_HOST="localhost"
8REVERB_PORT=8080
9REVERB_SCHEME=http
10
11VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
12VITE_REVERB_HOST="${REVERB_HOST}"
13VITE_REVERB_PORT="${REVERB_PORT}"
14VITE_REVERB_SCHEME="${REVERB_SCHEME}"
15

One more thing. Open up resources/js/echo.js file:

1import Echo from 'laravel-echo';
2
3import Pusher from 'pusher-js';
4window.Pusher = Pusher;
5
6window.Echo = new Echo({
7    broadcaster: 'reverb',
8    key: import.meta.env.VITE_REVERB_APP_KEY,
9    wsHost: import.meta.env.VITE_REVERB_HOST,
10    wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
11    wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
12    forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
13    enabledTransports: ['ws', 'wss'],
14});  


This file shows how Laravel Echo connects with the Reverb server. If you take a step further into the bootstrap.js file, you’ll see the echo file was imported. This means on start up, our app now uses and connects to reverb!

Show Workings – Test Reverb Connection

We have set up Laravel and Laravel Reverb. How do we test that our WebSocket server is working and the app is properly set up receive events on the Laravel frontend?

Step 1: Head over to the resources/js/echo.js file again. Here, we will set up a channel and tell it to listen to an event (doesn’t matter that we haven’t created it yet).

Add the following code to the file:

1/** 
2 * Testing Channels & Events & Connections
3 */
4window.Echo.channel("delivery").listen("PackageSent", (event) => {
5    console.log(event);
6});

Here, we have created a delivery channel & are listening on a PackageSent (This is imaginary for now) ****event.

Step 2: Restart the reverb server but with this command:

1php artisan reverb:start --debug
2

We added a debug option to allow us to see the logs of the WebSocket connections from the terminal. It’s also a good idea to debug your realtime connections problem if it’s not working as intended.

Step 3: Now, reload your Laravel app. Click on the Login or Dashboard link and open the chrome dev tools. Ensure you narrow it down to WS as shown below.

You should see the realtime connections and events in the devtools like so:

See the delivery channel we created. Now, you can also see the ping and pong events. This means our server is ready and waiting to stream real-time connections. Yaaay!

You can also see the evidence on the server. Check the console of the reverb debug. You should see something like this:

Step 4: Create the PackageSent event so that we can send events.

Run the following artisan command to create it quickly:

1php artisan make:event PackageSent
2

Open up the app/Events/PackageSent.php to see the event boilerplate created.

1<?php
2
3namespace App\\Events;
4
5use Illuminate\\Broadcasting\\Channel;
6use Illuminate\\Broadcasting\\InteractsWithSockets;
7use Illuminate\\Broadcasting\\PresenceChannel;
8use Illuminate\\Broadcasting\\PrivateChannel;
9use Illuminate\\Contracts\\Broadcasting\\ShouldBroadcast;
10use Illuminate\\Foundation\\Events\\Dispatchable;
11use Illuminate\\Queue\\SerializesModels;
12
13class PackageSent
14{
15    use Dispatchable, InteractsWithSockets, SerializesModels;
16
17    /**
18     * Create a new event instance.
19     */
20    public function __construct()
21    {
22        //
23    }
24
25    /**
26     * Get the channels the event should broadcast on.
27     *
28     * @return array<int, \\Illuminate\\Broadcasting\\Channel>
29     */
30    public function broadcastOn(): array
31    {
32        return [
33            new PrivateChannel('channel-name'),
34        ];
35    }
36}


Now, replace it with the code below:

app/Events/PackageSent.php

1<?php
2
3namespace App\\Events;
4
5use Illuminate\\Broadcasting\\Channel;
6use Illuminate\\Broadcasting\\InteractsWithSockets;
7use Illuminate\\Broadcasting\\PresenceChannel;
8use Illuminate\\Broadcasting\\PrivateChannel;
9use Illuminate\\Contracts\\Broadcasting\\ShouldBroadcast;
10use Illuminate\\Foundation\\Events\\Dispatchable;
11use Illuminate\\Queue\\SerializesModels;
12
13class PackageSent implements ShouldBroadCast
14{
15    use Dispatchable, InteractsWithSockets, SerializesModels;
16
17    /**
18     * Create a new event instance.
19     */
20    public function __construct(
21        public string $status,
22        public string $deliveryHandler
23    )
24    {
25        
26    }
27
28    /**
29     * Get the channels the event should broadcast on.
30     *
31     * @return Illuminate\\Broadcasting\\Channel
32     */
33    public function broadcastOn(): Channel
34    {
35        return new Channel('delivery');
36    }
37}

The following happened:

  • Now the class implements ShouldBroadCast. This means the event should be broadcasted via Laravel Echo.
  • Passed in two parameters to the constructor. Status of the package and the handler. We need this to know the status of the package and who is responsible for it at anytime.
  • The broadCastOn() method by default allows us to broadcast to many channels at a time. However, in this case we want to broadcast to only one. So it was modified to return only one channel; delivery , instead of an array of channels.

Note: This is a public channel. We are broadcasting the PackageSent event on a public channel. The channels are either instances of ChannelPrivateChannel, or PresenceChannel. PrivateChannel and PresenceChannel require authorization for any user to subscribe to while any random user can subscribe public channels.

Step 5: Dispatch the PackageSent event.

Open your terminal and fire up the amazing artisan tinker by running the following command:

1php artisan tinker
2

Just before we write code in the tinker terminal. Open a new terminal and ensure your queue is running like so:

1php artisan queue:listen
2

Note: This is very important because event jobs are queued to the database by default. So our queue needs to be able to listen to the jobs to fire them.

Now call the event and dispatch it like so within the tinker terminal:

1> use App\\Events\\PackageSent;
2> PackageSent::dispatch('processed', 'prosper');
3
4> PackageSent::dispatch('delivered', 'olamide');

This will go ahead and fire the PackageSent event twice.

Check your queue console to see if the jobs were processed.

Yaaay, it was processed!

Now, go ahead and check your Laravel app in the dev tools console. You should see the dispatched event and the data we sent.

Woow! Just amazing!!

We’ve been doing this from the terminal. Next, let’s do this straight from the UI with user interaction.

Build a real-time delivery history UI

Open your terminal and run the command below to create a livewire component.

1php artisan make:livewire DeliveryHistory
2

Laravel will create a class and corresponding delivery-history blade view for the UI elements.

Open up resources/views/layouts/app.blade.php file:

Add the delivery-history livewire component just below the @if code block in the code like so:

1....
2<body class="font-sans antialiased">
3        <x-banner />
4
5        <div class="min-h-screen bg-gray-100 dark:bg-gray-900">
6            @livewire('navigation-menu')
7
8            <!-- Page Heading -->
9            @if (isset($header))
10                <header class="bg-white dark:bg-gray-800 shadow">
11                    <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
12                        {{ $header }}
13                    </div>
14                </header>
15            @endif
16
17            <livewire:delivery-history />
18
19            <!-- Page Content -->
20            <main>
21                {{ $slot }}
22            </main>
23           
24        </div>
25
26        @stack('modals')
27
28        @livewireScripts
29    </body>
30

Open up resources/views/livewire/delivery-history.blade.php file & let’s add a full blown UI to it. Copy the code below and add it:

1<div class="py-12">
2    <div class="space-y-4">
3        <div class="rounded-lg max-w-7xl mx-auto sm:px-6 lg:px-8 dark:bg-gray-800">
4            <div class="py-2">
5            <form wire:submit.prevent="submitStatus" class="flex gap-2">
6                <input type="text" placeholder="Enter delivery status....." wire:model="status" x-ref="statusInput" name="status" id="status" class="block w-full" />
7                <button class=" hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded">
8                  ENTER
9                </button>
10        </form>
11            </div>
12        </div>
13    </div>
14
15    <div class="mt-5">
16        <div class="rounded-lg max-w-7xl mx-auto sm:px-6 lg:px-8 dark:bg-gray-800">
17            <div class="flex items-center justify-between"><h5 class="forge-h5">Package Delivery History</h5></div>
18            <div class="py-2">
19                <table class="w-full text-left">
20
21                   @if( count($packageStatuses) > 0)
22                    <thead class="text-gray-500">
23                        <tr class="h-10">
24                            <th class="pr-4 font-normal">User</th>
25                            <th class="w-full pr-4 font-normal">Written Status</th>
26                            <th class="pr-4 font-normal">Time</th>
27                            <th class="pr-4 font-normal">Status</th>
28                            <th></th>
29                        </tr>
30                    </thead>
31                    <tbody class="max-w-full text-white">
32                        @foreach($packageStatuses as $status)
33                            <tr class="h-12 border-t border-gray-100 dark:border-gray-700">
34
35                            
36                                <td class="whitespace-nowrap pr-4">
37                                    <div class="flex items-center">
38                                        <div class="text-truncate w-32"> {{ $status['deliveryPersonnel'] }}</div>
39                                    </div>
40                                </td>
41                                <td class="whitespace-nowrap pr-4">
42                                    <div class="flex items-center">
43                                        <div class="text-truncate w-32">{{ $status['deliveryStatus'] }}</div>
44                                    </div>
45                                </td>
46                                <td class="whitespace-nowrap pr-4">
47                                    <div class="flex items-center">
48                                        <div class="text-truncate w-32">{{ Carbon\\Carbon::parse($status['deliveryTime'])->diffForHumans() }} </div>
49                                    </div>
50                                </td>
51                                <td class="whitespace-nowrap pr-4">
52                                    <div class="flex items-center">
53                                        <div class="text-truncate w-32">
54
55                                            @if ($status['deliveryStatus'] == 'Port')
56                                                <div class="h-2 rounded-full bg-blue-600 transition-all transition-2s ease-in-out">
57                                                </div>
58                                            @endif
59
60                                            @if ($status['deliveryStatus'] == 'Processing')
61                                                <div class="h-2 rounded-full bg-yellow-600 transition-all transition-2s ease-in-out">
62                                                </div>
63                                            @endif
64
65                                            @if ($status['deliveryStatus'] == 'Shipped')
66                                                <div class="h-2 rounded-full bg-pink-600 transition-all transition-2s ease-in-out">
67                                                </div>
68                                            @endif
69
70                                            @if ($status['deliveryStatus'] == 'Delivered')
71                                                <div class="h-2 rounded-full bg-green-600 transition-all transition-2s ease-in-out">
72                                                </div>
73                                            @endif
74
75                                            @if (!in_array($status['deliveryStatus'], ['Port', 'Processing', 'Shipped', 'Delivered']))
76                                                <div class="h-2 rounded-full bg-red-600 transition-all transition-2s ease-in-out">
77                                                </div>
78                                            @endif
79                                        </div>
80                                    </div>
81                                </td>   
82                            </tr>
83                        @endforeach
84                    </tbody>
85                   @else
86                    <h3> No History yet... </h3>
87                   @endif
88                </table>
89            </div>
90        </div>
91    </div>
92</div>


Let’s look at the code above for a bit and understand what’s going on:

  • There’s a livewire form field with an input text field and button.
  • When the button is clicked, it calls the submitStatus function. Now this function will be defined later in the DeliveryHistory class component.
  • In the table section, we are looping over a $packageStatuses array variable and displaying the contents in the UI.
  • If the $packageStatuses array is empty, we show a “No History yet…” section.

Wire up the delivery history class component

Open up the app/Livewire/DeliveryHistory.php class and replace the content with the code below:

1<?php
2
3namespace App\\Livewire;
4
5use Carbon\\Carbon;
6use Livewire\\Component;
7use Livewire\\Attributes\\On;
8use App\\Events\\PackageSent;
9
10class DeliveryHistory extends Component
11{
12    public array $packageStatuses = [
13    ];
14
15    public string $status = '';
16
17    public function submitStatus()
18    {
19        PackageSent::dispatch(auth()->user()->name, $this->status, Carbon::now());
20
21        $this->reset('status');
22    }
23
24    #[On('echo:delivery,PackageSent')]
25    public function onPackageSent($event)
26    {
27        $this->packageStatuses[] = $event;
28    }
29
30    public function render()
31    {
32        return view('livewire.delivery-history');
33    }
34}
35

Let’s break down what’s happening the code above and see how it connects with the blade UI.

  • The render() method fetches and display the content of the delivery-history blade file to the UI.
  • There are two class variables, $status and $packageStatuses. Livewire automatically makes them accessible from the corresponding blade view.
  • The submitStatus() method is called when the form is submitted from the UI via livewire. In this method, we dispatch the PackageSent event with 3 arguments. The logged-in user’s name, the value of the text field in the UI, and the current time. When the PackageSent event is dispatched, how do we get the result of the event real-time via Reverb?
  • Laravel livewire has a seamless way of retrieving real-time events with Laravel Echo via the On Attribute. We defined a function onPackageSent() that dumps the payload of the recently dispatched $event into the $packageStatuses array. The attribute #[On('echo:delivery,PackageSent')] makes it possible for us to specify the channel name and the event for livewire to listen to! Feels magical!

Modify the PackageSent Laravel event

Before we test the app, we need to modify the constructor arguments of the PackageSent event class.

Open up app/Events/PackageSent.php and modify the constructor to take in 3 arguments; $deliveryPersonnel, $deliveryStatus, $deliveryTime.

1<?php
2
3namespace App\\Events;
4
5use Illuminate\\Broadcasting\\Channel;
6use Illuminate\\Broadcasting\\InteractsWithSockets;
7use Illuminate\\Broadcasting\\PresenceChannel;
8use Illuminate\\Broadcasting\\PrivateChannel;
9use Illuminate\\Contracts\\Broadcasting\\ShouldBroadcast;
10use Illuminate\\Foundation\\Events\\Dispatchable;
11use Illuminate\\Queue\\SerializesModels;
12
13class PackageSent implements ShouldBroadCast
14{
15    use Dispatchable, InteractsWithSockets, SerializesModels;
16
17    /**
18     * Create a new event instance.
19     */
20    public function __construct(
21        public string $deliveryPersonnel,
22        public string $deliveryStatus,
23        public string $deliveryTime
24    )
25    {
26        
27    }
28
29    /**
30     * Get the channels the event should broadcast on.
31     *
32     * @return Illuminate\\Broadcasting\\Channel
33     */
34    public function broadcastOn(): Channel
35    {
36        return new Channel('delivery');
37    }
38}

Reload and test the app

Now, you can test the app.

Enter any status in the textfield and hit the Enter button OR hit Enter on your keyboard and watch everything come together.

Note: In the delivery history blade view, we defined a list of statuses: Port, Processing, Shipped, Delivered.

Test the app while firing events from the console

We have been able to test the app via the UI and it works really well!

Now, open up tinker again and fire the event and watch how the UI updates in realtime! 🎉

Broadcast events only to a private channel

We’ve been listening and broadcasting events on a public channel. Now, let’s see how to do this securely on a private channel.

We want to restrict subscription to our delivery channel to authorized users only. Let’s make some changes in several areas across the app.

Step 1: Change Channel to PrivateChannel in PackageSent Event.

1...
2/**
3 * Get the channels the event should broadcast on.
4 *
5 * @return Illuminate\\Broadcasting\\PrivateChannel
6 */
7public function broadcastOn(): Channel
8{
9    return new PrivateChannel('delivery');
10}
11...
12

Step 2: Instruct the Livewire DeliveryHistory Component to also listen on a Private Channel by changing the value from echo:delivery to echo-private:delivery in the On Attribute like so:

1...
2#[On('echo-private:delivery,PackageSent')]
3public function onPackageSent($event)
4{
5    $this->packageStatuses[] = $event;
6}
7...
8

Reload your app, you will see a 403 forbidden error now for WebSocket connections.

Step 3: Open up the routes/channels.php file. This is where the authorization logic resides to determine who can listen to a given channel. Replace the code there with the following:

1use Illuminate\\Support\\Facades\\Broadcast;
2
3Broadcast::channel('delivery', function ($user) {
4    return (int) $user->id === 1;
5});


In the above code, the channel method accepts two arguments: the name of the channel and a callback which returns true or false indicating whether the user is authorized to listen on the channel.

Here, we have instructed the app to permit and authorize only a logged-in user with ID 1 to listen to the delivery channel.

Now, ensure that the user with ID 1 is logged into the app and check if there’s a WebSocket forbidden error.

Viola! No error and we can listen on the channel!

Try logging with another user and see what happens.

Forbidden! This user is not authorized to listen on this channel.

Ideally, in a more robust scenario, each user should be subscribed to their own private channels. This method prevents users from accessing each other’s specific events. This is great for game rooms, chat rooms, log history specific boards, etc.

So your authorization might need to look like this:

1use Illuminate\\Support\\Facades\\Broadcast;
2
3Broadcast::channel('delivery.{id}', function ($user, $id) {
4    return (int) $user->id === $id;
5});
6

This means the following:

  • Authenticated user with ID 1 can only listen to channel delivery.1
  • Authenticated user with ID 2 can only listen to channel delivery.2
  • Authenticated user with ID 3 can only listen to channel delivery.3

All authorization callbacks receive the currently authenticated user as their first argument and any additional wildcard parameters as their subsequent arguments.

Take it further!

Laravel Reverb is a fantastic addition to the extensive collection of impressive developer packages in the Laravel ecosystem.

In this guide, I invite you to extend the app by adding a persistent layer for delivery statuses. When the event is triggered, it should also be stored in the database.

Next up: handle notifications in your Laravel Reverb app

I hope you found this guide enjoyable and informative. In the next section, I’ll demonstrate how to seamlessly integrate real-time notifications into your Laravel Reverb App.

Ready to add notifications to your Laravel app? Keep reading!

If you have an idea that requires real-time capabilities, Laravel and Reverb could be the perfect fit. You can find me on Discord and Twitter. Don’t hesitate to reach out.

Related Posts

category: How to

How to Build a Notion-Like Notification Inbox with Chakra UI and Novu

Learn how to build a Notion-inspired real-time notification inbox in React using Chakra UI and Novu's customizable notification component. Includes code examples, styling tips, and a live demo.

Emil Pearce
Emil Pearce
category: How to

Build a Real-time Notification System with Socket.IO and ReactJS

Learn how to build a real-time notification system in a chat app with ReactJS and Socket.io. This step-by-step guide covers setup, event handling, notifications, and best practices.

Emil Pearce
Emil Pearce
category: How to

Case Study: How Novu Migrated User Management to Clerk

Discover how Novu implemented Clerk for user management, enabling features like SAML SSO, OAuth providers, and multi-factor authentication. Learn about the challenges faced and the innovative solutions developed by our team. This case study provides insights into our process, integration strategies that made it possible.

Emil Pearce
Emil Pearce