July 8, 2026

Realtime in Lattice: the page decides what to listen for

By Manuel Christlieb — Staff Engineer

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.