The Ultimate Guide to Laravel Reverb

Prosper Otemuyiwa - Apr 9 - - Dev Community

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.

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.



laravel new carryon


Enter fullscreen mode Exit fullscreen mode

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.



php artisan migrate



Enter fullscreen mode Exit fullscreen mode

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! 🎉

New InstallationInstall 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:



php artisan install:broadcasting



Enter fullscreen mode Exit fullscreen mode

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



php artisan reverb:start


Enter fullscreen mode Exit fullscreen mode

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:



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



Enter fullscreen mode Exit fullscreen mode

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



npm run dev



Enter fullscreen mode Exit fullscreen mode

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.



BROADCAST_CONNECTION=reverb
...

REVERB_APP_ID=872050
REVERB_APP_KEY=ed5zsi5ebpdmawcqbwva
REVERB_APP_SECRET=zbttdgtacvuhfdo3dl0o
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http

VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"



Enter fullscreen mode Exit fullscreen mode

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



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 ?? 80,
    wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
    enabledTransports: ['ws', 'wss'],
});  


Enter fullscreen mode Exit fullscreen mode

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:



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


Enter fullscreen mode Exit fullscreen mode

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:



php artisan reverb:start --debug



Enter fullscreen mode Exit fullscreen mode

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:

Real-time eventsReal-time logSee 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:

Server Reverb logsStep 4: Create the PackageSent event so that we can send events.

Run the following artisan command to create it quickly:



php artisan make:event PackageSent


Enter fullscreen mode Exit fullscreen mode

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



<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class PackageSent
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('channel-name'),
        ];
    }
}


Enter fullscreen mode Exit fullscreen mode

Now, replace it with the code below:

app/Events/PackageSent.php



<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class PackageSent implements ShouldBroadCast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     */
    public function __construct(
        public string $status,
        public string $deliveryHandler
    )
    {

    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return Illuminate\Broadcasting\Channel
     */
    public function broadcastOn(): Channel
    {
        return new Channel('delivery');
    }
}


Enter fullscreen mode Exit fullscreen mode

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 Channel, PrivateChannel, 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:



php artisan tinker



Enter fullscreen mode Exit fullscreen mode

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



php artisan queue:listen


Enter fullscreen mode Exit fullscreen mode

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:



> use App\Events\PackageSent;
> PackageSent::dispatch('processed', 'prosper');

> PackageSent::dispatch('delivered', 'olamide');


Enter fullscreen mode Exit fullscreen mode

This will go ahead and fire the PackageSent event twice.

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

Job processingYaaay, 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.

EventsStream of eventsWoow! 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.



php artisan make:livewire DeliveryHistory



Enter fullscreen mode Exit fullscreen mode

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:



....
<body class="font-sans antialiased">
        <x-banner />

        <div class="min-h-screen bg-gray-100 dark:bg-gray-900">
            @livewire('navigation-menu')


            @if (isset($header))
                <header class="bg-white dark:bg-gray-800 shadow">
                    <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
                        {{ $header }}
                    </div>
                </header>
            @endif

            <livewire:delivery-history />


            <main>
                {{ $slot }}
            </main>

        </div>

        @stack('modals')

        @livewireScripts
    </body>



Enter fullscreen mode Exit fullscreen mode

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:



<div class="py-12">
    <div class="space-y-4">
        <div class="rounded-lg max-w-7xl mx-auto sm:px-6 lg:px-8 dark:bg-gray-800">
            <div class="py-2">
            <form wire:submit.prevent="submitStatus" class="flex gap-2">
                <input type="text" placeholder="Enter delivery status....." wire:model="status" x-ref="statusInput" name="status" id="status" class="block w-full" />
                <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">
                  ENTER
                </button>
        </form>
            </div>
        </div>
    </div>

    <div class="mt-5">
        <div class="rounded-lg max-w-7xl mx-auto sm:px-6 lg:px-8 dark:bg-gray-800">
            <div class="flex items-center justify-between"><h5 class="forge-h5">Package Delivery History</h5></div>
            <div class="py-2">
                <table class="w-full text-left">

                   @if( count($packageStatuses) > 0)
                    <thead class="text-gray-500">
                        <tr class="h-10">
                            <th class="pr-4 font-normal">User</th>
                            <th class="w-full pr-4 font-normal">Written Status</th>
                            <th class="pr-4 font-normal">Time</th>
                            <th class="pr-4 font-normal">Status</th>
                            <th></th>
                        </tr>
                    </thead>
                    <tbody class="max-w-full text-white">
                        @foreach($packageStatuses as $status)
                            <tr class="h-12 border-t border-gray-100 dark:border-gray-700">


                                <td class="whitespace-nowrap pr-4">
                                    <div class="flex items-center">
                                        <div class="text-truncate w-32"> {{ $status['deliveryPersonnel'] }}</div>
                                    </div>
                                </td>
                                <td class="whitespace-nowrap pr-4">
                                    <div class="flex items-center">
                                        <div class="text-truncate w-32">{{ $status['deliveryStatus'] }}</div>
                                    </div>
                                </td>
                                <td class="whitespace-nowrap pr-4">
                                    <div class="flex items-center">
                                        <div class="text-truncate w-32">{{ Carbon\\\\Carbon::parse($status['deliveryTime'])->diffForHumans() }} </div>
                                    </div>
                                </td>
                                <td class="whitespace-nowrap pr-4">
                                    <div class="flex items-center">
                                        <div class="text-truncate w-32">

                                            @if ($status['deliveryStatus'] == 'Port')
                                                <div class="h-2 rounded-full bg-blue-600 transition-all transition-2s ease-in-out">
                                                </div>
                                            @endif

                                            @if ($status['deliveryStatus'] == 'Processing')
                                                <div class="h-2 rounded-full bg-yellow-600 transition-all transition-2s ease-in-out">
                                                </div>
                                            @endif

                                            @if ($status['deliveryStatus'] == 'Shipped')
                                                <div class="h-2 rounded-full bg-pink-600 transition-all transition-2s ease-in-out">
                                                </div>
                                            @endif

                                            @if ($status['deliveryStatus'] == 'Delivered')
                                                <div class="h-2 rounded-full bg-green-600 transition-all transition-2s ease-in-out">
                                                </div>
                                            @endif

                                            @if (!in_array($status['deliveryStatus'], ['Port', 'Processing', 'Shipped', 'Delivered']))
                                                <div class="h-2 rounded-full bg-red-600 transition-all transition-2s ease-in-out">
                                                </div>
                                            @endif
                                        </div>
                                    </div>
                                </td>   
                            </tr>
                        @endforeach
                    </tbody>
                   @else
                    <h3> No History yet... </h3>
                   @endif
                </table>
            </div>
        </div>
    </div>
</div>


Enter fullscreen mode Exit fullscreen mode

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:



<?php

namespace App\Livewire;

use Carbon\Carbon;
use Livewire\Component;
use Livewire\Attributes\On;
use App\Events\PackageSent;

class DeliveryHistory extends Component
{
    public array $packageStatuses = [
    ];

    public string $status = '';

    public function submitStatus()
    {
        PackageSent::dispatch(auth()->user()->name, $this->status, Carbon::now());

        $this->reset('status');
    }

    #[On('echo:delivery,PackageSent')]
    public function onPackageSent($event)
    {
        $this->packageStatuses[] = $event;
    }

    public function render()
    {
        return view('livewire.delivery-history');
    }
}



Enter fullscreen mode Exit fullscreen mode

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.



<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class PackageSent implements ShouldBroadCast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     */
    public function __construct(
        public string $deliveryPersonnel,
        public string $deliveryStatus,
        public string $deliveryTime
    )
    {

    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return Illuminate\Broadcasting\Channel
     */
    public function broadcastOn(): Channel
    {
        return new Channel('delivery');
    }
}


Enter fullscreen mode Exit fullscreen mode

Reload and Test The App

Now, you can test the app.

Test the appEnter 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.

StatusesList of statusesTest 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! 🎉

Tinker processingBroadCast 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.



...
/**
 * Get the channels the event should broadcast on.
 *
 * @return Illuminate\Broadcasting\PrivateChannel
 */
public function broadcastOn(): Channel
{
    return new PrivateChannel('delivery');
}
...



Enter fullscreen mode Exit fullscreen mode

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:



...
#[On('echo-private:delivery,PackageSent')]
public function onPackageSent($event)
{
    $this->packageStatuses[] = $event;
}
...



Enter fullscreen mode Exit fullscreen mode

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

403 Forbidden ErrorStep 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:



use Illuminate\Support\Facades\Broadcast;

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


Enter fullscreen mode Exit fullscreen mode

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.

Channel authorizationViola! No error and we can listen on the channel!

Try logging with another user and see what happens.

Channel authentication and authorizationForbidden! 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:



use Illuminate\Support\Facades\Broadcast;

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



Enter fullscreen mode Exit fullscreen mode

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.

Stay tuned for more, as I'm confident you'll acquire new strategies and abilities that will assist you in building your next app!

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .