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:

  1. A broadcast driver in the host app (Pusher, Reverb, Soketi and so on), set up the usual Laravel way. Until then Laravel's null driver silently drops every event.
  2. Laravel Echo in the browser, set up as below.
  3. TEXTSTEM_ECHO=true in .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 private textstem.* 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.

esc