Notification Preferences
Let users control optional notifications without changing Laravel's native notification workflow.
Inertia Start includes a Notifications page in the user account area. Users can choose which optional notifications they want to receive, while application code and third-party packages continue to send notifications through Laravel's native notification system.
The starter kit currently exposes one notification type:
| Notification | Slug | Default |
|---|---|---|
| New login | new_login | Enabled |
The new-login email is provided by alajusticia/laravel-logins. Users receive it by default and can disable it from their account.
How notification preferences work
Notification types are declared in the App\Enums\NotificationType backed enum. User choices are stored as booleans in the users.notification_preferences JSON column.
Each configurable notification implements App\Contracts\HasNotificationType and returns one enum case. Before Laravel sends any notification channel, App\Listeners\CheckNotificationPreference handles Laravel's NotificationSending event and checks the recipient's preference.
This interception is transparent to the code sending the notification:
$user->notify(new InvoiceReady($invoice));It also applies to notifications sent through the Notification facade and to queued notifications. No custom notification helper is required.
Untyped notifications are always sent
A notification that does not implement HasNotificationType is not configurable and is always sent. The same applies when the recipient does not implement HasNotificationPreferences, including on-demand recipients.
Adding a notification type
Add the enum case
Add a stable, snake-case slug to App\Enums\NotificationType, then decide whether the notification should be enabled for users who have not saved an explicit preference.
enum NotificationType: string
{
case NewLogin = 'new_login';
case InvoiceReady = 'invoice_ready';
public function defaultEnabled(): bool
{
return match ($this) {
self::NewLogin,
self::InvoiceReady => true,
};
}
}Missing JSON keys fall back to defaultEnabled(). This makes newly introduced types safe for existing users whose stored preferences predate the new enum case.
Add the labels and description
The Notifications account page resolves its content from the enum slug. Add the new keys to every supported locale:
'notifications' => [
'types' => [
'invoice_ready' => [
'title' => 'Invoice ready',
'description' => 'Receive an email when a new invoice is available.',
],
],
],Inertia Start currently ships English, French, and Spanish translations.
Assign the notification to the type
Implement HasNotificationType on every notification that should follow this preference:
namespace App\Notifications;
use App\Contracts\HasNotificationType;
use App\Enums\NotificationType;
use Illuminate\Notifications\Notification;
class InvoiceReady extends Notification implements HasNotificationType
{
public function notificationType(): NotificationType
{
return NotificationType::InvoiceReady;
}
// Define via(), toMail(), and the other notification methods normally.
}Multiple notification classes may return the same enum case when they should be controlled by one user preference.
Send it normally
Continue using Laravel's standard APIs:
$user->notify(new InvoiceReady($invoice));
Notification::send($users, new InvoiceReady($invoice));The listener evaluates the preference separately for every recipient and delivery channel.
Notification email footers
Notification emails include an unsubscribe slot in the footer, explaining why the recipient is receiving the email and, when applicable, how to unsubscribe.
This slot is resolved from the notification class itself, in both mail templates shipped by the starter kit: resources/views/mail/base.blade.php (used by the built-in notifications) and resources/views/vendor/notifications/email.blade.php (used by Laravel's default notification mail).
| Notification | Footer links to |
|---|---|
Implements HasNotificationType | The Notifications account page, where the user can turn it off |
| Untyped | Account profile page, since there is nothing to opt out of |
A notification can still provide its own footer through the unsubscribe view data. Pass your own text to replace the default sentence, or false to remove it entirely:
return (new MailMessage)
->subject(__('notifications.account_deletion.confirmation.subject'))
->markdown('mail.base', [
'beforeActionView' => 'mail.user.deleted',
'unsubscribe' => false,
]);Contact address
When I_S_CONTACT_EMAIL is set, the footer also invites recipients to write to that address, and the same address is used as the global Reply-To header for all outgoing emails. See Configuration.
Third-party notifications
When a package lets you replace its notification class, extend the package notification and implement HasNotificationType on the application class. Keep the package's normal notification dispatch enabled.
The built-in login integration follows this pattern:
use ALajusticia\Logins\Notifications\NewLogin;
use App\Contracts\HasNotificationType;
use App\Enums\NotificationType;
class LoggedIn extends NewLogin implements HasNotificationType
{
public function notificationType(): NotificationType
{
return NotificationType::NewLogin;
}
}alajusticia/laravel-logins still calls Laravel's native notify() method. The shared listener applies the user's preference without adding package-specific delivery logic.
Testing notification preferences
Laravel's Notification::fake() records notification attempts before the NotificationSending event is dispatched. It is useful for testing that application code requested a notification, but it does not exercise the preference listener.
To test preference enforcement, use a non-delivering mail transport such as Laravel's array mailer and assert the NotificationSent event, or test CheckNotificationPreference directly. Include coverage for:
- the default preference
- enabled and disabled preferences
- notifications without a type
- queued notifications when applicable
Key files
| File | Responsibility |
|---|---|
app/Enums/NotificationType.php | Available slugs, defaults, and translated account-page content |
app/Contracts/HasNotificationType.php | Assigns a notification to a configurable type |
app/Contracts/HasNotificationPreferences.php | Defines preference lookup for notifiable models |
app/Listeners/CheckNotificationPreference.php | Cancels disabled notification delivery through NotificationSending |
app/Models/User.php | Resolves defaults and persists user preferences |
app/Http/Controllers/Account/NotificationController.php | Displays and updates the Notifications account page |
resources/js/pages/account/notifications/Notifications.vue | Notification preference form |