July 11, 2026

Notifications in Lattice: a feature that is just another payload

By Manuel Christlieb — Staff Engineer

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.