The Ultimate Guide to Laravel Reverb
Tags: webdev, laravel, php, notifications
Published: 2024-04-09 10:11:13

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](https://github.com/novuhq/laravel-reverb-app)** on GitHub. Let's get started!

Install a Fresh Laravel App
---------------------------

Go ahead and create a new laravel app.


```bash
laravel 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](https://bit.ly/3UvPjrF).

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 &amp; access tokens table.


```bash
php artisan migrate

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

<figure class="wp-block-image size-large">![New Installation](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-04-at-05.59.38@2x-1024x572.png)</figure>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:


```bash
php 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


```bash
php artisan reverb:start
```
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:


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

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


```bash
npm run dev

```
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`.


```javascript
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}"

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


```javascript
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'],
});  
```
  
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:


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

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

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


```bash
php artisan reverb:start --debug

```
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:

<figure class="wp-block-image size-large">![Real-time events](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-04-at-06.53.37@2x-1024x573.png)</figure><figure class="wp-block-image size-large">![Real-time log](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-04-at-06.55.11@2x-1024x608.png)</figure>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:

<figure class="wp-block-image size-large">![Server Reverb logs](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-04-at-06.54.50@2x-1024x762.png)</figure>**Step 4:** Create the PackageSent event so that we can send events.

Run the following artisan command to create it quickly:


```bash
php artisan make:event PackageSent
```

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


```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
{
    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'),
        ];
    }
}
```
  
Now, replace it with the code below:

`app/Events/PackageSent.php`


```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');
    }
}
```
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:


```bash
php artisan tinker

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


```bash
php artisan queue:listen
```

**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:


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

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

<figure class="wp-block-image size-large">![Job processing](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-04-at-07.26.52@2x-1024x357.png)</figure>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.

<figure class="wp-block-image size-large">![Events](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-04-at-07.25.58@2x-1024x592.png)</figure><figure class="wp-block-image size-large">![Stream of events](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-04-at-07.53.24@2x-1024x575.png)</figure>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.


```bash
php artisan make:livewire DeliveryHistory

```
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:


```php
....
<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>

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


```php
<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>
```
  
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
<?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');
    }
}

```
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](https://livewire.laravel.com/docs/events#listening-for-echo-events). 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
<?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');
    }
}
```
Reload and Test The App
-----------------------

Now, you can test the app.

<figure class="wp-block-image size-large">![Test the app](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-04-at-18.38.31@2x-1024x609.png)</figure>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`.

<figure class="wp-block-image size-large">![Statuses](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-04-at-18.40.58@2x-1024x615.png)</figure><figure class="wp-block-image size-large">![List of statuses](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-04-at-18.42.34@2x-1024x615.png)</figure>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! 🎉

<figure class="wp-block-image size-large">![Tinker processing](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-04-at-19.07.04@2x-1024x462.png)</figure>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.


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

```
**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:


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

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

<figure class="wp-block-image size-large">![403 Forbidden Error](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-05-at-17.23.53@2x-1024x605.png)</figure>**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:


```php
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('delivery', function ($user) {
    return (int) $user->id === 1;
});
```
  
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.

<figure class="wp-block-image size-large">![Channel authorization](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-05-at-18.00.09@2x-1024x529.png)</figure>Viola! No error and we can listen on the channel!

Try logging with another user and see what happens.

<figure class="wp-block-image size-large">![Channel authentication and authorization](https://manage.novu.co/wp-content/uploads/2024/04/CleanShot-2024-04-05-at-18.02.23@2x-1024x555.png)</figure>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:


```php
use Illuminate\Support\Facades\Broadcast;

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

```
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](https://github.com/novuhq/laravel-reverb-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](https://bit.ly/3vULbrL) and [Twitter](https://bit.ly/44axm4W). Don't hesitate to reach out.