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
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
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:
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
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:
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 & instant.
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
.
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:
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:
/**
* Testing Channels & Events & Connections
*/
window.Echo.channel("delivery").listen("PackageSent", (event) => {
console.log(event);
});
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
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:
php artisan make:event PackageSent
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'),
];
}
}
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');
}
}
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
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
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');
This will go ahead and fire the PackageSent
event twice.
Check your queue console to see if the jobs were 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.
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
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>
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>
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 theDeliveryHistory
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');
}
}
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 thePackageSent
event with 3 arguments. The logged-in user’s name, the value of the text field in the UI, and the current time. When thePackageSent
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');
}
}
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.
...
/**
* 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:
...
#[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.
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:
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.
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:
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 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.
Top comments (1)
Thanks, It works local but in real live ubuntu with apache server it doesnt work