PHPackages                             offload-project/laravel-notification-preferences - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. offload-project/laravel-notification-preferences

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

offload-project/laravel-notification-preferences
================================================

Manage and display user notification preferences in Laravel

v1.5.0(2w ago)71.4k↓78%1MITPHPPHP ^8.3CI passing

Since Dec 16Pushed 2w agoCompare

[ Source](https://github.com/offload-project/laravel-notification-preferences)[ Packagist](https://packagist.org/packages/offload-project/laravel-notification-preferences)[ Docs](https://github.com/offload-project/laravel-notification-preferences)[ RSS](/packages/offload-project-laravel-notification-preferences/feed)WikiDiscussions main Synced yesterday

READMEChangelog (7)Dependencies (30)Versions (10)Used By (1)

Laravel Notification Preferences
================================

[](#laravel-notification-preferences)

[![Latest Version on Packagist](https://camo.githubusercontent.com/6968bf7d47a845458128e04d5cb93cdae3afe3ff96290770c5e5d80f5c38d9c6/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6f66666c6f61642d70726f6a6563742f6c61726176656c2d6e6f74696669636174696f6e2d707265666572656e6365732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/offload-project/laravel-notification-preferences)[![Tests](https://camo.githubusercontent.com/9802381c70ccbee66301ffb172e271deda74737d3da0b903388fe08b0b195dfc/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6f66666c6f61642d70726f6a6563742f6c61726176656c2d6e6f74696669636174696f6e2d707265666572656e6365732f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/offload-project/laravel-notification-preferences/actions/workflows/tests.yml)[![Build](https://camo.githubusercontent.com/55914bf0dff021f679f4a7df439a28d1634068f21a46b19dd1e80ded47b26733/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6f66666c6f61642d70726f6a6563742f6c61726176656c2d6e6f74696669636174696f6e2d707265666572656e6365732f72656c656173652e796d6c3f6c6162656c3d6275696c64267374796c653d666c61742d737175617265)](https://github.com/offload-project/laravel-notification-preferences/actions/workflows/release.yml)[![Total Downloads](https://camo.githubusercontent.com/c50a193e8984c474774ec76fd3de79565b02d2bd7b2ca5b5d8fef7d53b7c72aa/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6f66666c6f61642d70726f6a6563742f6c61726176656c2d6e6f74696669636174696f6e2d707265666572656e6365732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/offload-project/laravel-notification-preferences)[![License: MIT](https://camo.githubusercontent.com/6c711032aff1ca0eb6b211aa6cb3649ce7fd64a7714e1181d4bb457f9680e7cf/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e2e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)

A Laravel package for managing user notification preferences with support for multiple channels, notification groups, and automatic channel filtering — perfect for building notification settings UIs.

Features
--------

[](#features)

- **Automatic Filtering** — All notifications respect user preferences without code changes
- **Multiple Channels** — Support for mail, database, broadcast, SMS, or custom channels
- **Notification Grouping** — Organize notifications into logical groups (system, marketing, etc.)
- **Forced Channels** — Critical notifications that users cannot disable
- **Permission Gating** — Hook into Laravel's Gate to hide and block notifications users aren't authorized to receive
- **Bulk Operations** — Disable all emails, mute a group, or toggle notification types
- **Structured Output** — UI-ready table structure for building preference pages
- **Opt-in/Opt-out Defaults** — Configure default behavior at global, group, or notification level
- **Event Dispatching** — Listen for preference changes for audit logging or sync
- **Email Unsubscribe Links** — Signed URLs for one-click unsubscribe with `List-Unsubscribe` header support
- **Input Validation** — Prevents setting preferences for unregistered notifications/channels

Table of Contents
-----------------

[](#table-of-contents)

- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Managing Preferences](#managing-preferences)
- [Bulk Operations](#bulk-operations)
- [Explicit Control with Trait](#explicit-control-with-trait)
- [Forced Channels](#forced-channels)
- [Permission Gating](#permission-gating)
- [Per-Channel Defaults](#per-channel-defaults)
- [Events](#events)
- [Using the Facade](#using-the-facade)
- [Using the Interface](#using-the-interface)
- [Table Structure Output](#table-structure-output)
- [Inertia.js Integration](#inertiajs-integration)
- [Cache Management](#cache-management)
- [Exception Handling](#exception-handling)
- [Email Unsubscribe Links](#email-unsubscribe-links)
- [Uninstalling](#uninstalling)
- [Configuration Reference](#configuration-reference)
- [AI Coding Assistant Skill](#ai-coding-assistant-skill)
- [Testing](#testing)
- [Contributing](#contributing)
- [Security](#security)
- [License](#license)

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 11/12/13

Installation
------------

[](#installation)

```
composer require offload-project/laravel-notification-preferences
```

Publish the config and migrations:

```
php artisan vendor:publish --tag=notification-preferences-config
php artisan vendor:publish --tag=notification-preferences-migrations
php artisan migrate
```

Quick Start
-----------

[](#quick-start)

**1. Add the trait to your User model:**

```
use OffloadProject\NotificationPreferences\Concerns\HasNotificationPreferences;

class User extends Authenticatable
{
    use HasNotificationPreferences;
}
```

**2. Register your notifications** in `config/notification-preferences.php`:

```
return [
    'channels' => [
        'mail' => ['label' => 'Email', 'enabled' => true],
        'database' => ['label' => 'In-App', 'enabled' => true],
    ],

    'groups' => [
        'system' => [
            'label' => 'System Notifications',
            'description' => 'Important system updates',
            'default_preference' => 'opt_in',
            'order' => 1,
        ],
    ],

    'notifications' => [
        \App\Notifications\OrderShipped::class => [
            'group' => 'system',
            'label' => 'Order Shipped',
            'description' => 'When your order ships',
            'order' => 1,
        ],
    ],
];
```

**3. Send notifications normally** — preferences are applied automatically:

```
$user->notify(new OrderShipped($order));
```

Managing Preferences
--------------------

[](#managing-preferences)

```
// Set a preference
$user->setNotificationPreference(OrderShipped::class, 'mail', false);

// Check a preference
$enabled = $user->getNotificationPreference(OrderShipped::class, 'mail');

// Get all preferences
$preferences = $user->getNotificationPreferences();

// Get structured table for UI
$table = $user->getNotificationPreferencesTable();
```

Bulk Operations
---------------

[](#bulk-operations)

Convenient methods for "disable all emails" or "mute marketing" features:

```
// Disable all emails
$user->setChannelPreferences('mail', false);

// Mute all marketing notifications for email
$user->setGroupPreferences('marketing', 'mail', false);

// Disable all channels for a notification type
$user->setNotificationChannelPreferences(OrderShipped::class, false);

// Reset all preferences to defaults
$user->resetNotificationPreferences();
```

All bulk methods return the count of updated preferences and automatically skip forced channels.

Explicit Control with Trait
---------------------------

[](#explicit-control-with-trait)

For granular control, use the `ChecksNotificationPreferences` trait in your notification:

```
use OffloadProject\NotificationPreferences\Concerns\ChecksNotificationPreferences;

class OrderShipped extends Notification
{
    use ChecksNotificationPreferences;

    public function via($notifiable)
    {
        return $this->allowedChannels($notifiable, ['mail', 'database', 'broadcast']);
    }
}
```

Forced Channels
---------------

[](#forced-channels)

Prevent users from disabling critical notifications:

```
'notifications' => [
    SecurityAlert::class => [
        'group' => 'security',
        'label' => 'Security Alerts',
        'force_channels' => ['mail', 'database'],
    ],
],
```

Permission Gating
-----------------

[](#permission-gating)

Tie notifications to a Gate ability so users only see — and only receive — notifications they're authorized for. Works with native policies, Spatie Permission, Bouncer, or any package that registers with Laravel's Gate.

**Option 1: Implement the interface** (recommended for notifications you own):

```
use OffloadProject\NotificationPreferences\Contracts\AuthorizesNotification;

class OrderShipped extends Notification implements AuthorizesNotification
{
    public static function notificationAbility(): string
    {
        return 'view-orders';
    }
}
```

**Option 2: Declare the ability in config** (for notifications from other packages):

```
'notifications' => [
    \Vendor\Package\SomeNotification::class => [
        'group' => 'system',
        'label' => 'Vendor notification',
        'ability' => 'view-orders',
    ],
],
```

When a user fails the Gate check:

- Dispatch is blocked across **all channels** — authorization overrides forced channels and self-filtering traits.
- The notification is hidden from `getPreferencesTable()` so the UI doesn't expose toggles for things they can't access.
- A `NotificationAuthorizationDenied` event is dispatched per blocked channel for observability.

The notification instance is passed to your Gate closure at dispatch time, so you can authorize against the payload:

```
Gate::define('view-orders', function (User $user, ?OrderShipped $notification = null) {
    // $notification is null when called from the preferences UI (no instance exists)
    if ($notification) {
        return $user->can('view', $notification->order);
    }
    return $user->can('view-orders');
});
```

Per-Channel Defaults
--------------------

[](#per-channel-defaults)

Set specific channels enabled by default:

```
'notifications' => [
    OrderShipped::class => [
        'group' => 'system',
        'label' => 'Order Shipped',
        'default_channels' => ['mail', 'database'], // Only these enabled by default
    ],
],
```

Events
------

[](#events)

The package dispatches events when preferences change:

```
use OffloadProject\NotificationPreferences\Events\NotificationPreferenceChanged;

Event::listen(NotificationPreferenceChanged::class, function ($event) {
    // $event->preference - The NotificationPreference model
    // $event->user - The user who changed the preference
    // $event->wasCreated - Whether this was a new preference or update
});
```

`NotificationAuthorizationDenied` is dispatched whenever a notification is blocked by a failing Gate check ( see [Permission Gating](#permission-gating)):

```
use OffloadProject\NotificationPreferences\Events\NotificationAuthorizationDenied;

Event::listen(NotificationAuthorizationDenied::class, function ($event) {
    // $event->notifiable   - The user the notification was being sent to
    // $event->notification - The notification instance
    // $event->channel      - The channel that was blocked
    // $event->ability      - The Gate ability that failed
});
```

Using the Facade
----------------

[](#using-the-facade)

For quick access without dependency injection:

```
use OffloadProject\NotificationPreferences\Facades\NotificationPreferences;

// Check if a channel is enabled
NotificationPreferences::isChannelEnabled($user, OrderShipped::class, 'mail');

// Set a preference
NotificationPreferences::setPreference($user, OrderShipped::class, 'mail', false);

// Get structured table for UI
NotificationPreferences::getPreferencesTable($user);

// Discover registered configuration
NotificationPreferences::getRegisteredChannels();    // ['mail', 'database']
NotificationPreferences::getRegisteredGroups();      // ['system', 'marketing']
NotificationPreferences::getRegisteredNotifications(); // [OrderShipped::class, ...]
```

Using the Interface
-------------------

[](#using-the-interface)

For dependency injection and testing, use the interface:

```
use OffloadProject\NotificationPreferences\Contracts\NotificationPreferenceManagerInterface;

class NotificationPreferenceController
{
    public function __construct(
        private NotificationPreferenceManagerInterface $manager
    ) {}

    public function update(Request $request)
    {
        $this->manager->setPreference(
            $request->user(),
            $request->notification_type,
            $request->channel,
            $request->enabled
        );
    }
}
```

Table Structure Output
----------------------

[](#table-structure-output)

The `getNotificationPreferencesTable()` method returns UI-ready data:

```
[
    [
        'group' => 'system',
        'label' => 'System Notifications',
        'description' => 'Important system updates',
        'notifications' => [
            [
                'type' => 'App\Notifications\OrderShipped',
                'label' => 'Order Shipped',
                'description' => 'When your order ships',
                'channels' => [
                    'mail' => ['enabled' => true, 'forced' => false],
                    'database' => ['enabled' => true, 'forced' => false],
                ],
            ],
        ],
    ],
]
```

Inertia.js Integration
----------------------

[](#inertiajs-integration)

Share preferences via middleware:

```
// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
    return [
        ...parent::share($request),
        'notificationPreferences' => fn () => $request->user()?->getNotificationPreferencesTable(),
    ];
}
```

Cache Management
----------------

[](#cache-management)

Preferences are cached for performance (default: 24 hours). Configure the TTL in your config:

```
// config/notification-preferences.php
'cache_ttl' => 1440, // minutes (default: 24 hours)
```

Clear caches when needed:

```
use OffloadProject\NotificationPreferences\Contracts\NotificationPreferenceManagerInterface;

$manager = app(NotificationPreferenceManagerInterface::class);

// Clear all cached preferences for a user
$manager->clearUserCache($userId);

// Clear the memoized config cache (useful after runtime config changes)
$manager->clearConfigCache();
```

Exception Handling
------------------

[](#exception-handling)

The package validates all inputs and throws specific exceptions with helpful messages:

```
use OffloadProject\NotificationPreferences\Exceptions\InvalidNotificationTypeException;
use OffloadProject\NotificationPreferences\Exceptions\InvalidChannelException;
use OffloadProject\NotificationPreferences\Exceptions\InvalidGroupException;

try {
    $user->setNotificationPreference('UnregisteredNotification', 'mail', false);
} catch (InvalidNotificationTypeException $e) {
    // "Notification type 'UnregisteredNotification' is not registered...
    //  Add it to the 'notifications' array in 'config/notification-preferences.php'."
}

try {
    $user->setNotificationPreference(OrderShipped::class, 'sms', false);
} catch (InvalidChannelException $e) {
    // "Channel 'sms' is not registered... Available channels: mail, database."
}

try {
    $user->setGroupPreferences('nonexistent', 'mail', false);
} catch (InvalidGroupException $e) {
    // "Group 'nonexistent' is not registered... Available groups: system, marketing."
}
```

Email Unsubscribe Links
-----------------------

[](#email-unsubscribe-links)

The package can generate signed URLs that let users unsubscribe directly from emails — no login required. It also supports `List-Unsubscribe` headers for native unsubscribe buttons in Gmail, Apple Mail, and other clients (RFC 8058).

### Adding Unsubscribe Links to Notifications

[](#adding-unsubscribe-links-to-notifications)

Use the `HasUnsubscribeUrl` trait on your notification class:

```
use OffloadProject\NotificationPreferences\Concerns\HasUnsubscribeUrl;

class OrderShipped extends Notification
{
    use HasUnsubscribeUrl;

    public function toMail($notifiable): MailMessage
    {
        return $this->withUnsubscribeHeaders(
            (new MailMessage)
                ->line('Your order has shipped!')
                ->action('Unsubscribe', $this->getUnsubscribeUrl($notifiable)),
            $notifiable
        );
    }
}
```

`withUnsubscribeHeaders()` adds `List-Unsubscribe` and `List-Unsubscribe-Post` headers so email clients can show native unsubscribe buttons. The `getUnsubscribeUrl()` method generates a signed URL you can place anywhere in the email body.

### Generating URLs Directly

[](#generating-urls-directly)

You can also generate URLs from the user model or facade:

```
// From the user model
$url = $user->notificationUnsubscribeUrl(OrderShipped::class);
$url = $user->notificationResubscribeUrl(OrderShipped::class);

// From the facade
use OffloadProject\NotificationPreferences\Facades\NotificationPreferences;

$url = NotificationPreferences::unsubscribeUrl($user, OrderShipped::class);
$url = NotificationPreferences::resubscribeUrl($user, OrderShipped::class);

// For a specific channel (defaults to 'mail')
$url = NotificationPreferences::unsubscribeUrl($user, OrderShipped::class, 'database');
```

### How It Works

[](#how-it-works)

When a user clicks the unsubscribe link, the package:

1. Validates the signed URL (tamper-proof, no auth required)
2. Disables the notification type for that channel
3. Returns a JSON response, or redirects to your configured URL

The POST method is also supported for RFC 8058 one-click unsubscribe from email clients.

### Configuration

[](#configuration)

```
// config/notification-preferences.php
'unsubscribe' => [
    // Whether to register unsubscribe routes
    'enabled' => true,

    // Route prefix for unsubscribe/resubscribe endpoints
    'route_prefix' => 'notification-preferences',

    // Middleware for the unsubscribe routes
    'middleware' => ['web'],

    // Signed URL TTL in minutes (null for permanent/non-expiring)
    'url_ttl' => null,

    // Redirect to this URL after unsubscribing (with status, notification_type, and channel query params)
    // Set to null to return a JSON response instead
    'redirect_url' => null,

    // Enable resubscribe functionality
    'resubscribe_enabled' => true,
],
```

### Redirect Example

[](#redirect-example)

When `redirect_url` is set, users are redirected after unsubscribing:

```
'redirect_url' => '/notification-settings',
// Redirects to: /notification-settings?status=unsubscribed&notification_type=App\Notifications\OrderShipped&channel=mail
```

This lets you handle the confirmation page in your own frontend (Blade, Inertia, Livewire, etc.).

Uninstalling
------------

[](#uninstalling)

```
php artisan notification-preferences:uninstall --force
composer remove offload-project/laravel-notification-preferences
rm config/notification-preferences.php
```

Configuration Reference
-----------------------

[](#configuration-reference)

### Global Options

[](#global-options)

OptionTypeDescription`default_preference`string`opt_in` or `opt_out` for all notifications`cache_ttl`intCache duration in minutes (default: 1440 = 24h)`table_name`stringDatabase table name (default: notification\_preferences)`user_model`stringUser model class (default: App\\Models\\User)### Channels

[](#channels)

OptionTypeDescription`label`stringDisplay name for UI`enabled`boolWhether channel is available (default: true)### Groups

[](#groups)

OptionTypeDescription`label`stringDisplay name for UI`description`stringOptional description for UI`default_preference`string`opt_in` or `opt_out` (overrides global)`order`intSort order in UI### Notifications

[](#notifications)

OptionTypeDescription`group`stringGroup key this notification belongs to`label`stringDisplay name for UI`description`stringOptional description for UI`default_preference`string`opt_in` or `opt_out` (overrides group)`default_channels`arraySpecific channels enabled by default`force_channels`arrayChannels that cannot be disabled`ability`stringGate ability the user must pass to receive (also hides from preferences UI). Prefer the `AuthorizesNotification` interface for notifications you own.`order`intSort order within group### Unsubscribe

[](#unsubscribe)

OptionTypeDescription`enabled`boolRegister unsubscribe routes (default: true)`route_prefix`stringURL prefix for routes (default: notification-preferences)`middleware`arrayMiddleware stack for routes (default: \['web'\])`url_ttl`int|nullSigned URL expiration in minutes (null = permanent)`redirect_url`string|nullRedirect after action, or null for JSON response`resubscribe_enabled`boolRegister resubscribe route (default: true)AI Coding Assistant Skill
-------------------------

[](#ai-coding-assistant-skill)

This package ships a [Laravel Boost](https://skills.laravel.cloud/) skill so coding assistants (Claude Code, Cursor, etc.) follow the package's conventions when generating code. Install it in your app with:

```
php artisan boost:add-skill offload-project/laravel-notification-preferences
```

The skill source lives at [`skills/SKILL.md`](skills/SKILL.md).

Testing
-------

[](#testing)

```
composer test
```

Contributing
------------

[](#contributing)

Contributions are welcome! Please see the documents below before getting started.

- [Contributing Guide](CONTRIBUTING.md) — setup, workflow, commit conventions, and PR process
- [Code of Conduct](CODE_OF_CONDUCT.md) — expectations for participation in this project

Security
--------

[](#security)

- [Security Policy](SECURITY.md) — how to report a vulnerability privately

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

50

—

FairBetter than 95% of packages

Maintenance96

Actively maintained with recent releases

Popularity24

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity56

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 75.9% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~30 days

Recently: every ~19 days

Total

7

Last Release

19d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/331bcc01f75f46dded875d74f3db055da6d74be5f8820f0a29105c6a2cd8afc8?d=identicon)[shavonn](/maintainers/shavonn)

---

Top Contributors

[![shavonn](https://avatars.githubusercontent.com/u/3074595?v=4)](https://github.com/shavonn "shavonn (22 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (7 commits)")

---

Tags

laravelnotificationpreferences

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/offload-project-laravel-notification-preferences/health.svg)

```
[![Health](https://phpackages.com/badges/offload-project-laravel-notification-preferences/health.svg)](https://phpackages.com/packages/offload-project-laravel-notification-preferences)
```

###  Alternatives

[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.6k29.9M146](/packages/laravel-cashier)[psalm/plugin-laravel

Psalm plugin for Laravel

3355.3M345](/packages/psalm-plugin-laravel)[spatie/laravel-health

Monitor the health of a Laravel application

87512.0M164](/packages/spatie-laravel-health)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M194](/packages/laravel-ai)[yajra/laravel-oci8

Oracle DB driver for Laravel via OCI8

8793.2M25](/packages/yajra-laravel-oci8)[glushkovds/phpclickhouse-laravel

Adapter of the most popular library https://github.com/smi2/phpClickHouse to Laravel

2051.5M2](/packages/glushkovds-phpclickhouse-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
