Skip to content

Broadcasting Turbo Streams Over WebSockets With Laravel Echo

Introduction

So far, we've seen how to generate Turbo Streams to either add it to our Blade views or return them from controllers after a form submission over HTTP. In addition to that, you may also broadcast model changes over WebSockets (or Server-Sent Events) to all users that are viewing the same page. Although nice, you don't have to use WebSockets if you don't have the need for it. You may still benefit from Turbo Streams over HTTP.

The key here is that we'd broadcast those exact same Turbo Stream tags we've seen before. Remember, "HTML over the wire." Turbo Stream Broadcasts use Laravel Echo and Laravel's Broadcasting system.

Since broadcasts are commonly triggered after a form submission from one user, I'd still recommend feeding that specific user back with Turbo Streams (or a redirect and let Turbo refresh/morph) and only send the Turbo Stream broadcasts to other users most of the time. This way, the user making the change will have an instant feedback compared to having to wait for a background worker to pick up the job and send it to them over WebSockets.

Configuration

Broadcasting Turbo Streams relies heavily on Laravel's Broadcasting component. This means you need to configure Laravel Echo in the frontend and either use Pusher or any other open-source Pusher alternatives you may prefer. If you're not using Pusher, we recommend Soketi since it's easy to setup.

Listening to Broadcasts

Turbo Laravel will publish a custom HTML tag to your application's resources/js/elements folder. This tag is called <turbo-echo-stream-source> (see here).

You may add this tag to any Blade view passing the channel you want to listen to and users will start receiving Turbo Stream Broadcasts right away:

<turbo-echo-stream-source
channel="App.Models.Post.{{ $post->id }}"
/>

For convenience, you may prefer using the <x-turbo::stream-from> Blade component that ships with Turbo Laravel (it requires that you have a custom element named <turbo-echo-stream-source> available, since that's the tag this component will render). You may pass the model as the source prop to it, it will figure out the channel name for that specific model using Laravel's conventions:

<x-turbo::stream-from :source="$post" />

By default, it expects a private channel, so it must be used in a page where users are already authenticated. You may control the channel type in the tag with a type attribute.

<x-turbo::stream-from :source="$post" type="public" />

Make sure you have the Broadcast Auth Route for your models registered in your routes/channels.php file:

use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
 
Broadcast::channel(Post::class, function (User $user, Post $post) {
return $user->belongsToTeam($post->team);
});

You may want to read the Laravel Broadcasting documentation.

Broadcasting Model Changes

To be broadcast model changes for a particular, you must add the Broadcasts trait to your models:

use HotwiredLaravel\TurboLaravel\Models\Broadcasts;
 
class Comment extends Model
{
use Broadcasts;
}

This trait will augment any model with Turbo Stream broadacsting methods that you may use to trigger broadcasts. Here's how you can broadcast an append Turbo Stream for a newly created comment to all users visiting the post page:

Route::post('posts/{post}/comments', function (Post $post) {
$comment = $post->comments()->create(/** params */);
 
$comment->broadcastAppend()->toOthers()->later();
 
if (request()->wantsTurboStream()) {
return turbo_stream($comment);
}
 
return back();
});

Here are the methods now available to your model:

$comment->broadcastAppend();
$comment->broadcastPrepend();
$comment->broadcastBefore('target_dom_id');
$comment->broadcastAfter('target_dom_id');
$comment->broadcastReplace();
$comment->broadcastUpdate();
$comment->broadcastRemove();
$comment->broadcastRefresh();

These methods will assume you want to broadcast to your model's channel. However, you may want to send these broadcasts to a related model's channel instead:

$comment->broadcastAppendTo($post);
$comment->broadcastPrependTo($post);
$comment->broadcastBeforeTo($post, 'target_dom_id');
$comment->broadcastAfterTo($post, 'target_dom_id');
$comment->broadcastReplaceTo($post);
$comment->broadcastUpdateTo($post);
$comment->broadcastRemoveTo($post);
$comment->broadcastRefreshTo($post);

These broadcastXTo() methods accept either a model, an instance of the Channel class, or an array containing both of these. When it receives a model, it will guess the channel name using Laravel's Broadcasting channel naming convention.

All of these broadcasting methods return an instance of the PendingBroadcast class that will only dispatch the broadcasting job when that pending object is being garbage collected. Which means you may make changes to this pending broadcast by chaining on the returned object:

$comment->broadcastAppend()
->to($post)
->view('comments/_custom_view_partial', [
'comment' => $comment,
'post' => $post,
])
->toOthers() // Do not send to the current user...
->later(); // Don't send it now, dispatch a job to send in background instead...

You may want to hook these broadcasts from your model's events to trigger Turbo Stream broadcasts whenever your models are changed in any context:

class Comment extends Model
{
use Broadcasts;
 
protected static function booted()
{
static::created(function (Comment $comment) {
$comment->broadcastPrependTo($comment->post)->later();
});
 
static::updated(function (Comment $comment) {
$comment->broadcastReplaceTo($comment->post)->later();
});
 
static::deleted(function (Comment $comment) {
$comment->broadcastRemoveTo($comment->post)->later();
});
}
}

For convenience, instead of adding all these lines to achieve this set of broadcasting, you may add a $broadcasts = true property to your model class. This property instructs the Brodcasts trait to automatically hook the model Tubro Stram broadcasts on the correct events:

class Comment extends Model
{
use Broadcasts;
 
protected $broadcasts = true;
}

This achieves almost the same set of Broadcasts as the previous example, with a few nuanced differences. First, by default, it will broadcast an append Turbo Stream on newly created models. You may want to use prepend instead. You may do that by changing the $broadcasts property to be a configuration array instead of a boolean true, then set the insertsBy key to prepend:

class Comment extends Model
{
use Broadcasts;
 
protected $broadcasts = [
'insertsBy' => 'prepend',
];
}

When using the $broadcasts property, the Turbo Stream broadcasts will be sent to the current model's channel. However, since the channels use the model's ID as per the naming convention, no one will ever be able to listen on that channel before the model is created. For that reason, Turbo Stream broadcasts of newly created models will be sent to a private channel using the model's plural name instead. You may also configure which stream name this specific Turbo Stream should be sent to by setting the stream key on the $broadcasts property:

class Comment extends Model
{
use Broadcasts;
 
protected $broadcasts = [
'insertsBy' => 'prepend',
'stream' => 'my-comments',
];
}

This will send the Turbo Stream broadcast to private channel called my-comments when a new comment is created.

Alternatively, you may also set a $broadcastsTo proprety with either a string with the name of the relationship to be used to resolve the channel, or an array of relationships if you want to send the broadcast to multiple related model's channels:

class Comment extends Model
{
use Broadcasts;
 
protected $broadcasts = [
'insertsBy' => 'prepend',
];
 
protected $broadcastsTo = 'post';
 
public function post()
{
return $this->belongsTo(Post::class);
}
}

You may also do that by adding a broadcastsTo() method to your model instead of the $broadcastsTo property. The method must return either an Eloquent model, a Channel instance, or an array with a mix of those:

use Illuminate\Broadcasting\Channel;
 
class Comment extends Model
{
use Broadcasts;
 
protected $broadcasts = [
'insertsBy' => 'prepend',
];
 
public function post()
{
return $this->belongsTo(Post::class);
}
 
public function broadcastsTo()
{
return [
$this,
$this->post,
new Channel('full-control'),
];
}
}

Having a $broadcastsTo property or implementing the broadcastsTo() method in your model will have precedence over the stream key of the $broadcasts property.

Broadcasting Page Refreshes

Similar to the $broadcasts property, you may want to automatically configure page refresh broadcasts on a modal. You may use the $broadcastsRefreshes property for that:

use Illuminate\Broadcasting\Channel;
 
class Comment extends Model
{
use Broadcasts;
 
protected $broadcastsRefreshes = true;
}

This is the same as doing:

use Illuminate\Broadcasting\Channel;
 
class Comment extends Model
{
use Broadcasts;
 
public static function booted()
{
static::created(function ($comment) {
$comment->broadcastRefreshTo("comments")->later();
});
 
static::updated(function ($comment) {
$comment->broadcastRefresh()->later();
});
 
static::deleted(function ($comment) {
$comment->broadcastRefresh();
});
}
}

You may want to broadcast page refreshes to a related model:

use Illuminate\Broadcasting\Channel;
 
class Comment extends Model
{
use Broadcasts;
 
protected $broadcastsRefreshes = true;
 
protected $broadcastsRefreshesTo = ['post'];
 
public function post()
{
return $this->belongsTo(Post::class);
}
}

This will send page refreshes broadcasts to the related Post model channel.

Alternatively, you may specific a broadcastsRefreshesTo method instead of a property:

use Illuminate\Broadcasting\Channel;
 
class Comment extends Model
{
use Broadcasts;
 
protected $broadcastsRefreshes = true;
 
public function post()
{
return $this->belongsTo(Post::class);
}
 
public function broadcastsRefreshesTo()
{
return [$this->post];
}
}

From this method, you may return an instance of an Eloquent model, a string representing the channel name, or an instance of a Channel class.

Broadcasting Turbo Streams to Other Users Only

As mentioned erlier, you may want to feed the current user with Turbo Streams using HTTP requests and only send the broadcasts to other users. You may achieve that by chaining on the pending broadcast object that returns from all broadcastX methods:

$comment->broadcastAppendTo($post)->toOthers();

Alternatively, you may use the Turbo Facade like so to configure a scope where all brodcasted Turbo Streams will be sent to other users only:

use HotwiredLaravel\TurboLaravel\Facades\Turbo;
 
Turbo::broadcastToOthers(function () {
// ...
});

If you always want to send broadcasts to other users excluding the current user from receiving broadcasts, you may call the broadcastToOthers without passing a closure to it somewhere globally like a middleware or the AppServiceProvider::boot() method:

<?php
 
namespace App\Providers;
 
use Illuminate\Support\ServiceProvider;
use HotwiredLaravel\TurboLaravel\Facades\Turbo;
 
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Turbo::broadcastToOthers();
}
}

This only applies to broadcasts generated in an HTTP request, because this relies on having the X-Socket-ID header in the request, which Laravel Echo sets automatically. Any broadcast generate from a queue worker, for instance, will always be broadcasted to all users listening on the broadcasted channels.

Handmade Broadcasts

You may want to broadcast something independently of a model. You may do so using the HotwiredLaravel\TurboLaravel\Facades\TurboStream Facade (if you're not into Facades, type-hinting the HotwiredLaravel\TurboLaravel\Broadcasting\Factory class should also work):

TurboStream::broadcastAppend(
content: __('Hello World'),
target: 'notifications',
channel: 'general',
);

Model broadcasts use this same abstraction under the hood, so you have similar methods available:

TurboStream::broadcastAppend();
TurboStream::broadcastPrepend();
TurboStream::broadcastBefore();
TurboStream::broadcastAfter();
TurboStream::broadcastUpdate();
TurboStream::broadcastReplace();
TurboStream::broadcastRemove();
TurboStream::broadcastRefresh();

All of these methods, except the broadcastRemove() and broadcastRefresh, accept a $content parameter that may be a View instance, an instance of the HtmlString class, or a simple string:

// Passing a view instance as content...
TurboStream::broadcastAppend(
content: view('layouts.notification', ['message' => 'Hello World']),
target: 'notifications',
channel: 'general',
);
 
// Passing an instance of the HtmlString class (won't be escaped by Blade)...
TurboStream::broadcastAppend(
content: new HtmlString('Hello World'),
target: 'notifications',
channel: 'general',
);
 
// Passing a simple string (will be escaped by Blade)...
TurboStream::broadcastAppend(
content: 'Hello World',
target: 'notifications',
channel: 'general',
);

You may also customize the Turbo Stream by chaining on the returned PendingBroadcast object:

TurboStream::broadcastAppend('Hello World')
->target('notifications')
->to('general');

As for the channel, you may pass a string that will be interpreted as a public channel name, an Eloquent model which will resolve to a private channel using that model's broadcasting channel convention, or instances of the Illuminate\Broadcasting\Channel class.

You may want to specify private or presence string channels instead of public ones:

TurboStream::broadcastAppend('Hello World')
->target('notifications')
->toPrivateChannel('user.123');
 
TurboStream::broadcastAppend('Hello World')
->target('notifications')
->toPresenceChannel('user.123');

Using the broadcastAction() will allow you to broadcast any custom Turbo Stream action, so you're not limited to the default ones when using this approach:

TurboStream::broadcastAction('scroll_to', target: 'todo_123');

Handmade Broadcasting Using The turbo_stream() Response Builder

One more alternative to broadcasting Turbo Streams is to call the broadcastTo() method on the returned object of the turbo_stream() function:

turbo_stream()
->append('notifications', 'Hello World')
->broadcastTo('general');

This will tap on the PendingTurboStreamResponse and create a PendingBroadcast from it. It's important to note that this will return the same PendingTurboStreamResponse, not the PendingBroadcast. If you want to configure the PendingBroadcast that will be generated, you must do that before calling the broadcastTo() method, but you may also pass a Closure as the second parameter:

turbo_stream()
->append('notifications', 'Hello World')
->broadcastTo('general', fn ($broadcast) => $broadcast->toOthers());

The first argument must be either a string, an Eloquent model, or an instance of the Illuminate\Broadcasting\Channel class as the channel:

turbo_stream($comment)
->broadcastTo($comment->post, fn ($broadcast) => $broadcast->toOthers());

Similarly to using the Facade, you may also want to broadcast to private or presence string channels like so:

// Broadcast to private channels...
turbo_stream()
->append('notifications', 'Hello World')
->broadcastToPrivateChannel('user.123', fn ($broadcast) => $broadcast->toOthers())
 
// Broadcast to presence channels...
turbo_stream()
->append('notifications', 'Hello World')
->broadcastToPresenceChannel('chat.123', fn ($broadcast) => $broadcast->toOthers());

Continue to Validation Response Redirects...