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. Its definition() method maps each case to an App\Services\NotificationTypeDefinition containing the default state and optional UI group slug. The enum's defaultEnabled() and group() accessors read from that definition. 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.
use App\Services\NotificationTypeDefinition;
enum NotificationType: string
{
case NewLogin = 'new_login';
case InvoiceReady = 'invoice_ready';
public function definition(): NotificationTypeDefinition
{
return match ($this) {
self::NewLogin,
self::InvoiceReady => new NotificationTypeDefinition(
defaultEnabled: true,
),
};
}
}Missing JSON keys fall back to the definition's defaultEnabled value. 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.
Grouping notification types
Use a group when several related notification types should appear together. Each notification remains an independent preference and continues to use its own NotificationType case.
use App\Services\NotificationTypeDefinition;
enum NotificationType: string
{
case NewLogin = 'new_login';
case InvoiceReady = 'invoice_ready';
case PaymentFailed = 'payment_failed';
public function definition(): NotificationTypeDefinition
{
return match ($this) {
self::NewLogin => new NotificationTypeDefinition(
defaultEnabled: true,
),
self::InvoiceReady,
self::PaymentFailed => new NotificationTypeDefinition(
defaultEnabled: true,
group: 'billing',
),
};
}
}Then add the group title and description to every supported locale:
'notifications' => [
'groups' => [
'billing' => [
'title' => 'Billing notifications',
'description' => 'Choose which billing emails you want to receive.',
],
],
'types' => [
'invoice_ready' => [
'title' => 'Invoice ready',
'description' => 'Receive an email when a new invoice is available.',
],
'payment_failed' => [
'title' => 'Payment failed',
'description' => 'Receive an email when a subscription payment fails.',
],
],
],The Notifications page creates one parent checkbox for each group. The parent is checked when every child is enabled, unchecked when every child is disabled, and indeterminate when only some children are enabled. Selecting the parent updates every child in the form.
Only the child slugs (invoice_ready and payment_failed in this example) are submitted and stored in users.notification_preferences; the billing group slug is never persisted and does not represent a notification type (its only purpose is to visually group the notification types in the UI).
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 and each type's definition |
app/Services/NotificationTypeDefinition.php | Default state and optional group for a notification type |
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 and resolves translated group metadata |
resources/js/pages/account/notifications/Notifications.vue | Notification preference form, including group parent controls |