codewiser / postie
Subscription management Laravel package
Requires
- php: ^8.1
- laravel/framework: >=10.0
Requires (Dev)
- orchestra/testbench: ^8.0
- phpunit/phpunit: ^9.6
Suggests
- codewiser/laravel-notifications: Brings notification message contract
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0.0
- v1.4.6
- v1.4.5
- v1.4.4
- v1.4.3
- v1.4.2
- v1.4.1
- v1.4.0
- v1.3.3
- v1.3.2
- v1.3.1
- v1.3.0
- v1.2.6
- v1.2.5
- v1.2.4
- v1.2.3
- v1.2.2
- v1.2.1
- v1.2.0
- v1.1.11
- v1.1.10
- v1.1.9
- 1.1.8
- v1.1.7
- v1.1.6
- v1.1.5
- v1.1.4
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.8
- v1.0.7
- v1.0.6
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- v0.1.12
- v0.1.11
- v0.1.10
- v0.1.9
- v0.1.8
- 0.1.7
- 0.1.6
- 0.1.5
- 0.1.2
- 0.1.1
- 0.1.0
This package is auto-updated.
Last update: 2026-09-16 13:12:08 UTC
README
Postie is a dashboard where users can manage their subscription preferences.
Every Notification in the application has a corresponding audience.
It doesn't mean that everyone in the audience will receive a notification,
but it is possible. Postie allows the user to decide which channels
should be used to deliver the notification.
Installation
Install Postie into your project using the Composer package manager:
composer require codewiser/postie
After installing Postie,
publish its resources (provider, assets, migrations) using the
postie:install Artisan command:
php artisan postie:install
Before running migrations, you may want to change the name of the table
that keeps user subscription preferences. The configuration file is published
separately with Laravel's vendor:publish command:
php artisan vendor:publish --tag=postie-config
Then see config/postie.php:
'table' => env('POSTIE_TABLE', 'postie'),
After you have configured the table name, run migrations:
php artisan migrate
To refresh Postie assets after an update, use the postie:publish Artisan command:
php artisan postie:publish
Postie UI strings and the default group name are translatable. Publish Postie translations to override them:
php artisan vendor:publish --tag=postie-translations
Configuration
After installing Postie, its service provider will be
located at App\Providers\PostieServiceProvider. Do not forget to register
it in the list of service providers.
Postie can discover notifications automatically:
any notification class in the app/Notifications directory that applies the
Channel(s) or Group(s) attribute is registered on its own.
To use a different directory, publish the postie config and set the
notifications_path option to an array of directories.
Replace the Notification::via() method with the
\Codewiser\Postie\Notifications\Traits\Channelization trait.
The Notification will use the delivery channels the user preferred.
namespace App\Notifications; use Codewiser\Postie\Attributes\Channel; use Illuminate\Notifications\Notification; use Codewiser\Postie\Notifications\Traits\Channelization; #[Channel('mail')] class DailyNewsNotification extends Notification { use Channelization; }
Class-level attributes are not inherited from a parent class: apply them to each Notification that must use them.
This is a minimal setup. All of the following is mostly about making the Web panel beautiful.
Subscription Object
Initially, it is enough to provide a list of channels supported by the notification. Additionally, you may define a notification subject and description.
namespace App\Notifications; use Codewiser\Postie\Attributes\Channel; use Codewiser\Postie\Attributes\Description; use Codewiser\Postie\Attributes\Subject; use Illuminate\Notifications\Notification; use Codewiser\Postie\Notifications\Traits\Channelization; #[Subject('Daily News Notification')] #[Description('Sends most interesting news digest')] #[Channel('mail')] class DailyNewsNotification extends Notification { use Channelization; }
If you don't like PHP attributes, or if you need a translatable
title/description, define subscriptions in PostieServiceProvider explicitly.
You may combine both approaches: notification attributes take precedence.
use Codewiser\Postie\Subscription; use Codewiser\Postie\PostieApplicationServiceProvider; class PostieServiceProvider extends PostieApplicationServiceProvider { public function notifications(): array { return [ Subscription::to(DailyNewsNotification::class) ->via('mail') ->title(__('Daily News Notification')) ->description(__('Sends most interesting news digest')), ]; } }
Channel Object
When you set up a Subscription, you may pass a channel as a simple string.
But there is a way to define a more complex channel representation.
You may use the \Codewiser\Postie\Channel object to describe a channel
with a custom title, icon, etc.
The easiest approach is to describe all available channels in the Service Provider. Here you set up channel default properties. Later you may override channel properties per notification.
use Codewiser\Postie\Channel; use Codewiser\Postie\PostieApplicationServiceProvider; class PostieServiceProvider extends PostieApplicationServiceProvider { public function channels(): array { return [ Channel::via('mail') ->icon('envelope') ->title(__('via email')) ->subtitle(__('Sends emails')) ->passive(), ]; } }
When a Subscription gets a channel by its name, the channel
definition inherits these default properties (title, icon, etc.)
unless they are overridden by the channel attribute or fluent method.
If a channel hasn't been described in the Service Provider,
it gets built-in defaults (title and icon derived from its name).
You may define the default state of a channel. If a channel is active, then all users will receive notifications through it until they unsubscribe. Conversely, if a channel is passive, users will not receive notifications through it until they subscribe.
Default channel state is active.
namespace App\Notifications; use Codewiser\Postie\Attributes\Channel; use Illuminate\Notifications\Notification; use Codewiser\Postie\Notifications\Traits\Channelization; #[Channel('mail', default: false)] class DailyNewsNotification extends Notification { use Channelization; }
You may apply the same properties to a few channels at once using the
Channels attribute:
use Codewiser\Postie\Attributes\Channels; #[Channels(['mail', 'telegram'], default: false)] class DailyNewsNotification extends Notification { use Channelization; }
If you want to disable the user's ability to manage channel preferences, you may hide the channel from the user interface, or just force the channel state.
namespace App\Notifications; use Codewiser\Postie\Attributes\Channel; use Illuminate\Notifications\Notification; use Codewiser\Postie\Notifications\Traits\Channelization; #[Channel('mail', forced: true)] #[Channel('database', hidden: true)] class DailyNewsNotification extends Notification { use Channelization; }
Channel availability
A channel is available to the user when the notifiable has a route to receive
notifications through it. Availability is resolved by the
routeNotificationFor() method — either a routeNotificationFor{Channel}()
method on the notifiable.
namespace App\Models; use Illuminate\Notifications\Notifiable; class User extends Authenticatable { use Notifiable; public function routeNotificationForTelegram(): ?string { return $this->telegram_chat_id; } }
When a channel requires the user to enable a route (for example, connecting a messenger account), give the channel a router URL:
use Codewiser\Postie\Channel; use Codewiser\Postie\PostieApplicationServiceProvider; class PostieServiceProvider extends PostieApplicationServiceProvider { public function channels(): array { return [ Channel::via('telegram')->router(route('telegram.start_bot')), ]; } }
Postie will provide a Configure screen to the dashboard, so the user can enable it and start receiving notifications.
Grouping Subscriptions
You may group subscriptions to create a side menu for the dashboard.
The easiest approach is to describe all groups in the service provider. Here you set up group default properties (icon, weight, channels, audience). Later a subscription may reference a group by its name and inherit these properties.
use Codewiser\Postie\Audience; use Codewiser\Postie\Group; public function groups(): array { return [ Group::make('My group')->via('mail', 'database') ]; }
In notifications() you may reference a predefined group by its name:
use Codewiser\Postie\Subscription; public function notifications(): array { return [ Subscription::to(DailyNewsNotification::class)->group('My group'), Subscription::to(NewOrderNotification::class)->group('My group'), ]; }
Or define a group right inside notifications():
public function notifications(): array { return [ // Define group and assign a few subscriptions to it. Group::make('My group') ->via('mail', 'database') ->add(DailyNewsNotification::class) ->add(Subscription::to(NewOrderNotification::class)), ]; }
When a subscription defines its own channels, those channels win: group channels are ignored.
Also, you may assign a subscription to a group using the attribute applied to a notification.
namespace App\Notifications; use Codewiser\Postie\Attributes\Group; use Illuminate\Notifications\Notification; use Codewiser\Postie\Notifications\Traits\Channelization; #[Group('Another group')] class DailyNewsNotification extends Notification { use Channelization; }
To assign a notification to a few groups at once, use the Groups attribute:
use Codewiser\Postie\Attributes\Groups; #[Groups(['Daily', 'Weekly'])] class DailyNewsNotification extends Notification { use Channelization; }
Subscriptions that are not assigned to any group fall into a special fallback group (named "Other" by default, translatable).
Audience
A notification may be scoped to an audience using the Audience attribute.
If the current user is not in the notification's audience, he or she cannot manage the subscription to the notification. If an audience is not set, it means that every user can manage the subscription to such a notification.
Predefine possible notifiables (audience) in the service provider.
Each audience has a name (a BackedEnum is supported), a title,
and a callback that returns a Builder of possible notifiables.
use Codewiser\Postie\Audience; public function audiences(): array { return [ Audience::make('managers', 'Managers')->with( fn() => User::query()->where('role', 'manager') ), ]; }
The subscription adopts the builder of the predefined audience, referenced by its name.
namespace App\Notifications; use Codewiser\Postie\Attributes\Audience; use Illuminate\Notifications\Notification; use Codewiser\Postie\Notifications\Traits\Channelization; #[Audience('managers')] class ManagersDigestNotification extends Notification { use Channelization; }
A Subscription or a Group may also reference a predefined audience by
name, or accept an Audience object, via the for() method:
use Codewiser\Postie\Audience; public function notifications(): array { return [ // Reference a predefined audience by name. Subscription::to(DigestNotification::class)->for('managers'), // Or use an audience object. Subscription::to(ReportNotification::class)->for( Audience::make('managers', 'Managers')->with( fn() => User::query()->where('role', 'manager') ) ), ]; }
When a subscription defines its own audience, that audience wins: group audiences are ignored. Otherwise, when the audience comes from groups, the notifiable must belong to every group audience.
Previewing Notifications
You may define a notification preview so the user can see how the notification will look.
Notification previews may be composed with model factories.
use Codewiser\Postie\Subscription; Subscription::to(DailyNewsNotification::class) ->preview(function (string $channel, object $notifiable) { $news = NewsItem::factory()->count(3)->make(); $notification = new DailyNewsNotification($news); return match ($channel) { 'mail' => $notification->toMail($notifiable), 'telegram' => $notification->toTelegram($notifiable), default => $notification->toArray($notifiable), }; });
Alternatively, you may define the preview inside the Notification class.
Mark a static method with the Preview attribute — Postie will use it
to compose the preview.
namespace App\Notifications; use Codewiser\Postie\Attributes\Preview; use Illuminate\Notifications\Notification; use Codewiser\Postie\Notifications\Traits\Channelization; class DailyNewsNotification extends Notification { use Channelization; #[Preview] public static function preview(): callable { return function (string $channel, object $notifiable) { $news = NewsItem::factory()->count(3)->make(); $notification = new static($news); return match ($channel) { 'mail' => $notification->toMail($notifiable), 'telegram' => $notification->toTelegram($notifiable), default => $notification->toArray($notifiable), }; }; } }
The
Previewattribute is inherited from parent classes of the notification.
Previews defined via the subscription preview() method take precedence
over the Preview attribute.
API
Postie ships a REST API. You may implement your own subscription management system.
The API is not registered by default — you include the routes yourself, so the core package stays free of any API footprint.
Routing setup
Add a register() method to your App\Providers\PostieServiceProvider:
use Illuminate\Support\Facades\Route; public function register(): void { parent::register(); Route::group([ 'domain' => config('postie.domain', null), 'prefix' => config('postie.path'), 'middleware' => ['api', 'auth:sanctum'], 'as' => 'postie.', ], function () { $this->loadRoutesFrom(__DIR__.'/../../vendor/codewiser/postie/routes/api.php'); }); }
Documentation
Interactive OpenAPI documentation (Redoc) is served alongside the dashboard, without any extra setup — the docs routes are part of the dashboard routes:
GET /postie/api— the Redoc page.GET /postie/api/openapi.yaml— the OpenAPI specification itself.
