Typed Broadcasting Contracts in Laravel Reverb | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Laravel Broadcasting with Reverb: Building Typed Event Contracts for WebSocket Channels        On this page       1. [  The Problem with Stringly-Typed Broadcasts ](#the-problem-with-stringly-typed-broadcasts)
2. [  Defining a Typed Broadcast Contract ](#defining-a-typed-broadcast-contract)
3. [  Channel Authorization with Typed Guards ](#channel-authorization-with-typed-guards)
4. [  Testing the Full Flow with Pest ](#testing-the-full-flow-with-pest)
5. [  Architectural Takeaways ](#architectural-takeaways)

  ![Laravel Broadcasting with Reverb: Building Typed Event Contracts for WebSocket Channels](https://cdn.msaied.com/444/e50bb90e38d98d08949583fdcf123e65.png)

  #laravel   #reverb   #broadcasting   #websockets   #pest  

 Laravel Broadcasting with Reverb: Building Typed Event Contracts for WebSocket Channels 
=========================================================================================

     19 Jul 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   The Problem with Stringly-Typed Broadcasts  ](#the-problem-with-stringly-typed-broadcasts)
2. [  02   Defining a Typed Broadcast Contract  ](#defining-a-typed-broadcast-contract)
3. [  03   Channel Authorization with Typed Guards  ](#channel-authorization-with-typed-guards)
4. [  04   Testing the Full Flow with Pest  ](#testing-the-full-flow-with-pest)
5. [  05   Architectural Takeaways  ](#architectural-takeaways)

 The Problem with Stringly-Typed Broadcasts
------------------------------------------

Most Laravel broadcasting tutorials stop at `event(new OrderShipped($order))` and a matching `Echo.private('orders.' + id)` on the frontend. That works until a channel name typo silently drops events in production, or a payload shape change breaks the JS client with no PHP-side warning.

The fix is treating broadcast events as first-class typed contracts — enforced on both the PHP emitter and the channel authorization layer.

Defining a Typed Broadcast Contract
-----------------------------------

Start with an interface that every broadcastable event in a bounded context must implement:

```php
namespace App\Broadcasting\Contracts;

interface BroadcastContract
{
    /** @return non-empty-string[] */
    public function broadcastOn(): array;

    /** @return array */
    public function broadcastWith(): array;

    public function broadcastAs(): string;
}

```

Now implement a concrete event:

```php
namespace App\Domain\Orders\Events;

use App\Broadcasting\Contracts\BroadcastContract;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;

final class OrderStatusUpdated implements ShouldBroadcastNow, BroadcastContract
{
    public function __construct(
        public readonly int $orderId,
        public readonly string $status,
        public readonly int $tenantId,
    ) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel("tenant.{$this->tenantId}.orders.{$this->orderId}")];
    }

    public function broadcastWith(): array
    {
        return [
            'order_id' => $this->orderId,
            'status'   => $this->status,
        ];
    }

    public function broadcastAs(): string
    {
        return 'order.status.updated';
    }
}

```

`ShouldBroadcastNow` skips the queue — useful for status updates where latency matters more than throughput.

Channel Authorization with Typed Guards
---------------------------------------

Define the channel in `routes/channels.php` and keep the authorization logic in a dedicated class:

```php
// routes/channels.php
Broadcast::channel(
    'tenant.{tenantId}.orders.{orderId}',
    App\Broadcasting\Channels\TenantOrderChannel::class
);

```

```php
namespace App\Broadcasting\Channels;

use App\Models\Order;
use App\Models\User;

final class TenantOrderChannel
{
    public function join(User $user, int $tenantId, int $orderId): bool
    {
        if ($user->tenant_id !== $tenantId) {
            return false;
        }

        return Order::query()
            ->where('id', $orderId)
            ->where('tenant_id', $tenantId)
            ->exists();
    }
}

```

Using an invokable class instead of a closure keeps the authorization logic testable in isolation and out of the route file.

Testing the Full Flow with Pest
-------------------------------

Laravel's `Event::fake()` and `Broadcasting::fake()` let you assert broadcast behavior without a live Reverb server:

```php
use App\Domain\Orders\Events\OrderStatusUpdated;
use Illuminate\Support\Facades\Broadcasting;
use Illuminate\Support\Facades\Event;

it('broadcasts order status update on the correct private channel', function () {
    Broadcasting::fake();

    $order = Order::factory()->for(
        Tenant::factory()->create(['id' => 42])
    )->create();

    event(new OrderStatusUpdated(
        orderId: $order->id,
        status: 'shipped',
        tenantId: 42,
    ));

    Broadcasting::assertSentTo(
        new \Illuminate\Broadcasting\PrivateChannel("tenant.42.orders.{$order->id}"),
        OrderStatusUpdated::class,
        fn ($event) => $event->status === 'shipped'
    );
});

```

For channel authorization, test the channel class directly:

```php
it('denies access to orders from a different tenant', function () {
    $user = User::factory()->create(['tenant_id' => 1]);
    $order = Order::factory()->create(['tenant_id' => 2]);

    $channel = new \App\Broadcasting\Channels\TenantOrderChannel();

    expect($channel->join($user, tenantId: 2, orderId: $order->id))->toBeFalse();
});

```

No HTTP request, no WebSocket handshake — pure unit test.

Architectural Takeaways
-----------------------

- **Define a `BroadcastContract` interface** so every event in a context is structurally consistent and statically analysable.
- **Use `broadcastAs()`** to decouple the PHP class name from the JS event name — rename classes freely without breaking clients.
- **Move channel auth to dedicated classes** — closures in `channels.php` are untestable and grow messy fast.
- **Prefer `ShouldBroadcastNow`** for user-facing status updates; reserve queued broadcasting for high-volume background events.
- **Test with `Broadcasting::fake()`** — assert channel, event name, and payload shape without infrastructure.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-broadcasting-with-reverb-building-typed-event-contracts-for-websocket-channels&text=Laravel+Broadcasting+with+Reverb%3A+Building+Typed+Event+Contracts+for+WebSocket+Channels) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-broadcasting-with-reverb-building-typed-event-contracts-for-websocket-channels) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  When should I use ShouldBroadcastNow versus ShouldBroadcast?        Use ShouldBroadcastNow when the event must reach the client immediately, such as a status update triggered by a user action. Use ShouldBroadcast (queued) for high-volume events like analytics pings or background job progress, where a small delay is acceptable and you want to protect your Reverb server from burst load. 

      Q02  How do I prevent channel name collisions in a multi-tenant app?        Prefix every channel with a tenant identifier, e.g. tenant.{tenantId}.resource.{id}. Enforce this in the BroadcastContract implementation and validate the tenantId in the channel authorization class, rejecting any user whose tenant_id does not match the channel parameter. 

      Q03  Does Broadcasting::fake() work with Reverb specifically?        Yes. Broadcasting::fake() intercepts the broadcast dispatch before it reaches any driver, including Reverb. Your assertions run against the in-memory fake, so the tests are driver-agnostic and require no running Reverb server. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Laravel + Python FastAPI: Image OCR Demo](https://cdn.msaied.com/447/5f3d87146fbf19957973cbc88c6c0155.png) Laravel Python FastAPI 

### Laravel + Python FastAPI: Image OCR Demo

Learn how to call a Python image OCR script from Laravel using FastAPI as the bridge. This premium tutorial wa...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 20 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-python-fastapi-image-ocr-demo) [ ![CQRS in Laravel Without a Framework: Commands, Handlers, and Read Models That Stay Lean](https://cdn.msaied.com/446/a05febe70b72107387f210e0f9eae089.png) laravel cqrs architecture 

### CQRS in Laravel Without a Framework: Commands, Handlers, and Read Models That Stay Lean

Skip the heavy event-sourcing libraries. This guide shows how to implement a practical CQRS layer in Laravel u...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 20 Jul 2026     1 min read  

  Read    

 ](https://msaied.com/articles/cqrs-in-laravel-without-a-framework-commands-handlers-and-read-models-that-stay-lean) [ ![Laravel Event Sourcing: Projections, Snapshots, and Replay Without the Framework Tax](https://cdn.msaied.com/445/b4f83e0b5da7c11e2fe942eebc1cad08.png) laravel event-sourcing ddd 

### Laravel Event Sourcing: Projections, Snapshots, and Replay Without the Framework Tax

Event sourcing in Laravel without a heavy framework. Build lean projectors, snapshot aggregates at scale, and...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 19 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-event-sourcing-projections-snapshots-and-replay-without-the-framework-tax) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
