overtrue/laravel-subscribe
A lightweight Laravel package that adds subscribe/unsubscribe behavior to Eloquent models via traits. Users can follow posts or any model with subscribe, unsubscribe, toggle and isSubscribed helpers, plus subscribers and subscriptions relations, a publishable migration and events.
"overtrue/laravel-subscribe": "^4.0" and stop reading. If you are on Laravel 13, composer require overtrue/laravel-subscribe:^5.0.config/subscribe.php (polymorphic ID/UUID settings), the package migration (the subscriptions table with its unique index), and the two traits — Subscriber (the actor, usually User) and Subscribable (the thing being subscribed to, e.g. Post, Channel).Subscriber to User and Subscribable to a target model, then $user->subscribe($post) / $user->unsubscribe($post) / $user->toggleSubscribe($post), and read state with $user->isSubscribed($post) or $post->isSubscribedBy($user). That covers the 80% case in about ten lines.vendor:publish tags. 5.0 added explicit coverage for configuration and migration publishing, so if publishing previously felt flaky in your setup, re-verify the tags you use in CI/provisioning scripts.Feature toggles read better than raw subscribe/unsubscribe. A single controller endpoint backed by toggleSubscribe() plus isSubscribed() for the render keeps the UI honest and avoids double-submit rows racing the unique index:
$user->toggleSubscribe($post);
return back()->with('subscribed', $user->isSubscribed($post));
Guard with authorization, not just existence. The package does not know your policy layer. Wrap mutations in $this->authorize('subscribe', $post) — otherwise subscribe() happily creates rows for targets the user should never see. Combine with route-model binding for the target.
Attach status to lists instead of looping. The 5.0 notes call out expanded coverage for attaching subscription status across models, collections and pagination inputs. When rendering an index, use the collection/paginator attachment path the package exposes rather than calling isSubscribed() per row — that is the difference between one query and N+1 on a page of 25. If you already have a hand-rolled loop, this is the single highest-value refactor after upgrading.
Prefer eager loading for counts and lists. $user->subscriptions and $post->subscribers are morph relations; withCount('subscribers') / load('subscriptions') on the index query keeps the same semantics without the per-row hit. Cache aggregate counts (subscriber totals on a channel page) in your own layer if the page is hot — the package won't do it for you.
React to events, don't fork the package. Subscribed / Unsubscribed events are part of the preserved API. Put notifications, feed fan-out, analytics and cache invalidation in listeners. This keeps your upgrade path to 6.x clean and is exactly what the "events preserved" guarantee in 5.0 is protecting.
Polymorphic IDs. With UUID/ULID primary keys on either side, configure the package's ID setting and keep the morph columns consistent with that type. Use a morph map (Relation::enforceMorphMap([...])) so stored subscribable_type values are stable aliases, not FQCNs — 5.0 expands polymorphic ID coverage, but it cannot retroactively fix type strings you already wrote.
Testing. Event::fake([Subscribed::class]) to assert intent without side effects, RefreshDatabase for the pivot table, and assert idempotency: subscribing twice should not produce two rows. Use assertDatabaseHas on subscriptions for the happy path and a policy test for the denial path.
vendor:publish over an existing app and then migrate is how you get a duplicate-table or a silently divergent schema. Publish on fresh installs only.php artisan config:clear.enforceMorphMap after rows exist leaves orphaned subscribable_type values that simply stop matching — lists go quietly empty instead of erroring. Decide the morph strategy before the first production subscription and migrate type values in one explicit pass if you change it.ShouldHandleEventsAfterCommit behave differently inside a wrapping transaction — relevant if you also do the lock-based toggle above.How can I help you explore Laravel packages today?