ylsideas/subscribable-notifications
Laravel package for subscribable notifications: users opt in or out of notification types, subscriptions are stored in the database, and notifications go only to subscribed users through customizable, queueable channels with per-user preferences and easy integration.
ylsideas/subscribable-notifications gives you a per-recipient subscription layer on top of Laravel's notification system, plus the plumbing (routes, signed URLs, storage) for one-click unsubscribe links in emails. The day-to-day value: you stop re-implementing "does this user want this email, and how do they opt out?" in every project.
Minimal setup:
composer require ylsideas/subscribable-notifications — v1.8.0 widens support to Laravel 13; it works the same on 10/11/12.php artisan vendor:publish --provider="YlsIdeas\SubscribableNotifications\SubscribableNotificationsServiceProvider" (or list tags with php artisan vendor:publish and pick the package's config/migrations).php artisan migrate to create the subscriptions table.User (or any notifiable model) so it gains subscribe/unsubscribe/is-subscribed helpers.php artisan route:list | grep -i subscrib.Where to look first, in order: config/subscribable-notifications.php (notification registry, route prefix/middleware, model bindings), then the registered routes, then the Subscription model and its polymorphic columns. The config file is the contract — most "it doesn't work" reports trace back to a notification class not being listed there.
First real use case: a weekly digest or product-update email where the footer link is a signed URL that flips a single subscription row, no login required. Wire one notification class end-to-end before touching the rest of your notification catalogue.
Register notification "types" centrally, then check before sending. Treat each notification class (or config alias) as a subscription key. The cleanest send path is filtering the recipient collection rather than checking inside the notification:
Notification::send(
User::query()->where('active', true)
->with('subscriptions') // avoid N+1 inside the filter
->get()
->filter->isSubscribedTo(WeeklyDigest::class),
new WeeklyDigest()
);
Belt-and-braces with shouldSend(). For queued notifications the decision is made twice — once when you dispatch, once when the worker runs. Implement shouldSend($notifiable, $channel) to re-check the subscription so an unsubscribe that lands between dispatch and delivery is honoured. Return false rather than throwing; a silent skip is the correct semantic here.
Scope subscriptions to a subscribable. The polymorphic subscribable side of the subscription record lets you model per-project / per-thread / per-team preferences ("mute this ticket"). Use the same notification class with different subscribables and let the package's resolver decide. Keep a deliberate convention: null subscribable = global preference, non-null = scoped override, with the scoped row winning.
Custom unsubscribe UX. The package's default route is fine for email footers, but if you need a branded preferences page, disable the package routes in config and build your own controller. Generate links with URL::temporarySignedRoute(...) and protect the route with Laravel's signed middleware — the signature is what makes the link safe without authentication, so don't hand-roll a plain token.
Testing. Notification::fake() plus assertions on who received it is the fastest regression net:
Notification::fake();
$this->post('/unsubscribe/...'); // or call the trait method directly
Notification::send($subscribed, new WeeklyDigest());
Notification::assertSentTo($subscribed, WeeklyDigest::class);
Notification::assertNotSentTo($unsubscribed, WeeklyDigest::class);
Also assert the row directly (assertDatabaseHas('subscriptions', [...])) — it catches trait/morph issues that faked notifications hide.
composer update won't move you to 1.8.0, run composer why-not ylsideas/subscribable-notifications 1.8.0 — you'll usually find a pinned illuminate/* constraint or a conflicting package rather than a real incompatibility.APP_URL. Behind a load balancer or on a queue worker in a different environment, the host/scheme can drift and every link 403s. Fix with URL::forceScheme('https') and correct TrustProxies, and regenerate any links already sitting in queued mail.Relation::enforceMorphMap(), existing subscribable_type values (stored as FQCNs) stop resolving. Backfill the column in the same deploy that turns the map on.channel column in your own table, or a via() that filters channels against a preference object.Subscription::whereDoesntHave('subscriber') on a schedule keeps the table honest.php artisan route:list) → is the queue worker on the same APP_URL. That sequence resolves nearly every report.Subscription model binding, and the trait's isSubscribedTo/subscribeTo/unsubscribeFrom methods — override the model in config rather than editing vendor code when you need extra columns.How can I help you explore Laravel packages today?