Broadcasting (WebSockets)
This document describes how the package uses Laravel Broadcasting to push real-time updates to the browser via WebSockets.
Overview
The package broadcasts server-side events (model lifecycle changes, AI job completions) over private Laravel channels. The browser receives them through Laravel Echo. Livewire components subscribe to relevant channels and update their state when events arrive, avoiding the need to poll.
The broadcast transport (Pusher, Reverb, Soketi, etc.) is configured by the host app. The package registers its own channels and events independently of the driver.
Turning it on
Live updates are off by default. Three things are needed:
- A broadcast driver in the host app (Pusher, Reverb, Soketi and so on), set up the usual Laravel way. Until then Laravel's
nulldriver silently drops every event. - Laravel Echo in the browser, set up as below.
TEXTSTEM_ECHO=truein.env. While it is false, the admin's Livewire components register no Echo listeners (ListensForBroadcasts), so pages without Echo don't log warnings. Keyword generation falls back to polling the database (see Polling Fallback).
TEXTSTEM_ECHO=true
The package registers the broadcast auth route (/broadcasting/auth) and its own channels through TextstemBroadcastServiceProvider, which TextstemServiceProvider loads for you. A host app that also calls Broadcast::routes() or loads its own routes/channels.php keeps working; definitions with the same name are simply set again.
Package versions from before 28 October 2026 didn't load
TextstemBroadcastServiceProvider, so privatetextstem.*channels could not be authorised. Update the package, or register the provider in your app.
Host App Setup
The client-side Echo initialisation must be done in the host app. A commented-out starting point is provided in resources/js/bootstrap.js:
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
forceTLS: true
});
Configure the matching driver credentials in the host app's .env:
BROADCAST_CONNECTION=pusher # BROADCAST_DRIVER on Laravel 10
PUSHER_APP_KEY=...
PUSHER_APP_SECRET=...
PUSHER_APP_ID=...
PUSHER_APP_CLUSTER=mt1
For self-hosted alternatives (Reverb, Soketi), set broadcaster and the relevant env vars accordingly.
Channel Registration
Channels are registered in routes/channels.php and loaded by TextstemBroadcastServiceProvider. All textstem.* channels require an authenticated user. No further resource-level authorisation is applied since they are only reachable through the admin UI, which is already behind auth middleware.
| Channel pattern | Type | Used by |
|---|---|---|
App.Models.User.{id} |
Private | Standard Laravel user presence |
textstem.{class}.{id} |
Private | ModelCreated, ModelUpdated, ModelDeleted |
textstem.{class}.description.{id} |
Private | ModelDescriptionUpdated |
textstem.{class}.keywords.{id} |
Private | ModelKeywordsUpdated |
textstem.Post.article.{postId} |
Private | ArticleGenerated (post-specific) |
textstem.article.generated |
Private | ArticleGenerated (no post ID) |
textstem |
Public | PageLoaded, PageLoading, SubscriberUpdated |
{class} is the short model name with the Medialight\Textstem\Models\ prefix stripped (e.g. WranglerPage, Asset, Post).
Broadcast Events
Model lifecycle events
All models that extend BaseModel (src/Models/BaseModel.php) automatically fire these three events via $dispatchesEvents:
| Event | File | Channel | Broadcast name |
|---|---|---|---|
ModelCreated |
src/Events/ModelCreated.php |
textstem.{Class}.{id} |
model.created |
ModelUpdated |
src/Events/ModelUpdated.php |
textstem.{Class}.{id} |
model.updated |
ModelDeleted |
src/Events/ModelDeleted.php |
textstem.{Class}.{id} |
model.deleted |
Payload for all three includes the full $model instance and a $type string (Created / Updated / Deleted).
AI completion events
These are dispatched by queue jobs after an OpenAI request completes:
| Event | File | Channel | Broadcast name | Dispatched by |
|---|---|---|---|---|
ModelDescriptionUpdated |
src/Events/ModelDescriptionUpdated.php |
textstem.{Class}.description.{id} |
model.description.updated |
GenerateAltTextJob, OpenAiModelRequest, CreateImageDescription |
ModelKeywordsUpdated |
src/Events/ModelKeywordsUpdated.php |
textstem.{Class}.keywords.{id} |
model.keywords.updated |
OpenAiKeywordsRequest |
ArticleGenerated |
src/Events/ArticleGenerated.php |
textstem.Post.article.{postId} or textstem.article.generated |
(default class name) | OpenAiArticleRequest |
ArticleGenerated uses the post-specific channel when a $postId is provided, and falls back to textstem.article.generated otherwise.
Page and subscriber events
These broadcast on the public textstem channel:
| Event | File | Dispatched by |
|---|---|---|
PageLoading |
src/Events/PageLoading.php |
Page render pipeline |
PageLoaded |
src/Events/PageLoaded.php |
Page render pipeline |
SubscriberUpdated |
src/Events/SubscriberUpdated.php |
Host app subscriber logic |
SubscriberUpdated references App\Models\Subscriber, a host-app model, not a package model.
Livewire Echo Listeners
Livewire components subscribe to private channels using the echo-private: listener key prefix. The component's getListeners() method returns a dynamic channel name based on the bound $target model.
| Component | File | Channel | Event | Handler |
|---|---|---|---|---|
CreateDescription |
src/Livewire/Inputs/CreateDescription.php |
textstem.WranglerPage.description.{id} |
model.description.updated |
descriptionUpdated() |
CreateImageDescription |
src/Livewire/Inputs/CreateImageDescription.php |
textstem.WranglerPage.description.{id} |
model.description.updated |
descriptionUpdated() |
CreatePostDescription |
src/Livewire/Inputs/CreatePostDescription.php |
textstem.Post.description.{id} |
model.description.updated |
descriptionUpdated() |
CreateKeywords |
src/Livewire/Inputs/CreateKeywords.php |
textstem.{Class}.keywords.{id} |
model.keywords.updated |
keywordsUpdated() |
The Livewire echo-private: syntax requires Laravel Echo to be configured in the host app. The channel name and event name are separated by a comma in the listener key (e.g. echo-private:textstem.WranglerPage.description.5,.model.description.updated).
Polling Fallback
CreateKeywords includes a polling fallback for environments where Echo is not configured. If the generate() action returns an empty result (indicating the job is queued), it dispatches poll-change to trigger periodic checkDescription() calls that poll the database directly until keywords appear or a retry limit (10) is reached.