Notifications in Lattice: a feature that is just another payload
Part 9 of the Building Lattice series. Part 8 covered realtime.
That closed on page-scoped realtime being the wrong shape for one thing: telling a single user they have something new, regardless of which page they’re looking at. That needs something that follows the user, not the screen — a notification bell. And the bell doesn’t need a new mental model: same bet as every other Lattice feature, a self-describing payload built in PHP that a component renders — actions and all.
It rides Laravel’s own notifications
Lattice doesn’t ship its own notifications table; it rides Laravel’s native one:
php artisan notifications:table
php artisan migrate
and the model receiving them needs the standard Notifiable trait — without
it there’s no $user->notify(...) to call. One prerequisite is easy to miss:
Lattice notifications are queued (ShouldQueue), so ->send() sits in the
queue until something processes it. A running php artisan queue:work is
required in production; locally, QUEUE_CONNECTION=sync runs them inline.
Sending one
Notification::make() is a fluent builder; ->send() or ->sendToDatabase()
dispatches the finished payload to any notifiable:
use Lattice\Lattice\Core\Enums\Variant;
use Lattice\Lattice\Notifications\Notification;
Notification::make()
->title('Order #1234 shipped')
->body('Tracking is now available.')
->icon('truck')
->variant(Variant::Success)
->href('/orders/1234')
->action(MarkOrderSeenAction::class, ['order_id' => 1234])
->send($order->user);
->title() and ->body() are headline and supporting text; ->icon() and
->variant() drive the bell item’s icon and accent color. The only real
decision is send versus sendToDatabase: ->send() delivers over both
database and broadcast, ->sendToDatabase() persists only — no realtime
push, just a row waiting for the next fetch or poll.
Actions that outlive their moment
->href($url) is the simple case: it makes the whole row a link, so clicking
the title or body follows an Inertia visit to $url and marks the
notification read. ->action() and ->link() are the more interesting case,
and the design point of this whole feature.
An action button can’t be built the way a page button is, because a
notification outlives the request that created it. By the time someone clicks
it, the reader isn’t necessarily the authorization context it was sent under.
So ->action() doesn’t attach a real button — it stores a descriptor:
the action’s class name and arguments. The real, signed Action is built
when the notification is fetched, using the reading user’s own
authorization context, and silently dropped from that row if the class is no
longer registered.
->link($label, $url) is the plainer sibling: a link button for a second
call to action alongside ->href().
Placing the bell
Notifications::make() drops into a Topbar like any other component:
use Lattice\Lattice\Layouts\Components\Topbar;
use Lattice\Lattice\Notifications\Components\Notifications;
Topbar::make('app-topbar')->sticky()->items([
Notifications::make()->slideOut(),
]);
It defaults to a popover anchored under the bell icon; ->slideOut() swaps
that for a full-height panel sliding in from the trailing edge. And it renders
any row in the notifications table best-effort, not only ones built with
the Lattice builder.
Reuse across channels
That last point makes the builder useful outside ->send(), too. A
notification that also needs mail or Slack doesn’t reach for a Lattice base
class — it’s a plain Laravel notification whose toArray() returns the
builder’s own ->toArray(), and the bell renders it exactly like anything
sent through ->send().
How it stays live
This ties back to Part 8, and it’s a genuinely
different mechanism. A page’s Listen listeners are scoped to whoever has
that page open; the bell needs to follow one user everywhere. So each
notifiable gets its own private channel —
NotificationChannel::for($notifiable) — and the bell subscribes to it with
useEchoNotification from @laravel/echo-react, prepending whatever arrives
with no page reload. If Echo isn’t installed or configured, the bell catches
that failure and falls back to plain polling instead of crashing — set
polling_interval in config, or ->pollingInterval($seconds) per bell, for
the fallback cadence. Where Listen is built for “everyone watching this
page,” the bell’s channel is built for one person, on every page. More in the
notifications docs.
The shape holds, again
Everything here has been Lattice’s own component — the builder, the bell, the channel, all supplied by the framework. That’s been true of most of this series: declare something in PHP, get a component for free. Next is the other side of that bet — what it takes to ship a component that isn’t one of Lattice’s own.