Realtime in Lattice: the page decides what to listen for
Part 8 of the Building Lattice series. Part 7 covered fragments.
Everything so far has been one direction: a request comes in, a page describes itself, React renders it. Realtime inverts that. There’s no request — an event happens somewhere else in the system, and the page has to have already told the server how it wants to react before that event exists. Lattice keeps this in the same place as everything else: you declare it in PHP, and the framework wires up the subscription.
Declaring listeners
Override protected function listeners(): array on a page and return one or
more Listen declarations — a channel, the broadcast event(s) to react to, and
the effects to run when one arrives:
use Lattice\Lattice\Realtime\Listen;
protected function listeners(): array
{
return [
// public channel, react to a single broadcast
Listen::channel('orders')
->on('.OrderShipped')
->toast('An order just shipped'),
// per-user private channel, several events, several effects
Listen::private('orders.'.auth()->id())
->on(['.OrderShipped', '.OrderDelivered'])
->toast('Your order was updated')
->reloadPage(),
];
}
Notice what listeners() doesn’t take: no Request. There’s nothing to inject —
by the time a broadcast arrives there’s no request in flight, so if you need the
current user, you reach for auth()->id() the way you would in a queued job, not
an injected parameter. ->on(...) takes one event or several; .OrderShipped
with the leading dot matches a broadcast name as-is, and dropping the dot uses
Laravel’s namespaced event class convention instead.
Channels
Three constructors, matching Laravel’s own channel types:
Listen::channel('orders'); // public
Listen::private('orders.42'); // private — requires channel authorization
Listen::presence('room.42'); // presence
channel() is open to anyone listening. private() and presence() go through
Laravel’s own channel authorization — the same routes/channels.php callback you’d
write for broadcasting without Lattice at all — so a per-user channel like
orders.42 only reaches the user it’s scoped to.
Effects are a smaller set on purpose
A listener’s effects are ->toast(...), ->callout(...) and ->reloadPage().
That’s the whole list. No redirect, no reloadComponent, no openModal — and
that’s deliberate, not a gap I haven’t gotten to. Those effects need request
context: a redirect needs somewhere to send the response, a modal needs a form to
render into. A broadcast doesn’t have either — it arrives on a socket, not a
request/response cycle, so there’s no response to redirect and no request-scoped
form to open. It’s the same discipline as fragments
dropping Request from schema(): constrain the signature to what the context
can actually supply, and let that constraint tell you which effects even make
sense here.
What the client needs
There’s nothing to render for a listener — no component, no markup. Lattice reads the listener declarations out of the page payload and subscribes to the channels itself. The only thing you set up by hand is Echo, once, at the app entry point:
import { configureEcho } from '@laravel/echo-react';
configureEcho({ broadcaster: 'reverb' /* …your Reverb config */ });
It’s built on Laravel Echo and a broadcasting backend such as Reverb — nothing
Lattice-specific to learn there. If a page declares listeners but Echo isn’t
configured, Lattice logs a warning and the page still works; it just doesn’t get
the realtime updates. And if you want it off entirely, there’s a global
kill-switch, lattice.realtime.enabled, that stops listeners from being
serialized to the client at all. More detail is in the
realtime docs.
Coarse by design
Page listeners are scoped to whoever’s looking at that page, and the effect list is small on purpose — a toast, a callout, a reload. That’s the right shape for “everyone watching this dashboard should know an order shipped.” It’s the wrong shape for “tell this one user they have a new notification,” which isn’t about a page at all — it needs something that follows the user, not the screen. That’s next.