masterix21/laravel-entitlements
A flexible Laravel package for managing feature entitlements, plans, and quotas. Define features, attach them to plans or Eloquent models, then check access, consume limited usage, and control what each user or team can do via a clean, expressive API. Ideal for SaaS feature gating.
composer require masterix21/laravel-entitlements, then publish and run migrations before touching any code:
php artisan vendor:publish --tag="entitlements-migrations"
php artisan vendor:publish --tag="entitlements-config"
php artisan migrate
If you are upgrading from 1.3.0 or earlier you must re-publish the migrations: 1.3.1 ships a new unique index on entitlement_plan_items (plan_id, type).lang/vendor/laravel-entitlements/, not the app's root lang/. Move any existing customised copies there.config/entitlements.php. Three keys do the heavy lifting day to day: type_enum (your backed enum of entitlement types), entitlements.models.* (model overrides), and the plan_transition settings that govern scheduled plan changes.resources/boost/guidelines/laravel-entitlements/ before prompting an AI assistant about this package — it encodes strategy semantics, invariants, and the exceptions reference that assistants otherwise guess wrong.type_enum, create a Plan with one PlanItem per type (the DB now enforces one item per type per plan), attach a License to a billable, consume and release a slot, then exercise Entitlements::changePlan() on the anchor license and drain it with Entitlements::applyDueTransitions().Override models through config, never by editing vendor code. Subclass the package License/Plan/Usage models and point entitlements.models.* at them. As of 1.3.1 these overrides are honoured consistently across the Entitlements service and every strategy — previously some internal queries bypassed them, so if you had workarounds (custom repositories, raw queries), revisit and delete them.
Treat slot release as a two-phase state machine. SlotStrategy::requestRelease() moves a usage into Releasing; something later finalises it. Don't hand-roll decrements of slot_used, and don't call release twice — the new locking + re-check means a released usage can no longer flip back to Releasing and double-decrement. Use forceRelease() for admin overrides; it now decrements by the usage amount rather than a hard-coded 1, so partial/multi-slot usages are correct.
Drive plan changes through the anchor license. Entitlements::changePlan() creates a pending PlanTransition on the anchor license (locking it first, so concurrent calls can't stack two pending transitions). Pair it with a scheduled command calling Entitlements::applyDueTransitions() and expose cancelTransition() to users/support. Both are declared on the facade docblock as of 1.3.1, so static analysis and IDE autocomplete now work.
Handle the documented exceptions as first-class UI states, not 500s: IncompatiblePlanTransition (empty target plan, downgrade below current usage, including computed types) and TransitionAlreadyResolved (someone else already applied/cancelled it). Validate in your form requests where you can, and convert the rest into notifications.
Assume listeners run after commit. Domain events dispatch via DB::afterCommit. Keep listeners side-effect-safe and queue long work; they never fire on rollback, so anything that assumed "log the attempt regardless" must move into the command that owns the transaction.
Filament: register the plugin on your panel and let it own the plan resource. The 1.3.1 fixes are worth relying on — the item-type repeater blocks duplicate types, plan delete/bulk delete is blocked while licenses or transitions reference the plan, "Force Release Slot" now sweeps the whole license group (anchor + children), and the change-plan select keeps the current plan visible but disabled. For your own custom actions, mirror those guards, because the package now enforces them in two places and you don't want a third inconsistent path.
Computed entitlements: register a resolver per computed type and treat it as part of the type registry — downgrade validation consults it in 1.3.1. A missing resolver now surfaces as a config validation error rather than a silent miscount.
select plan_id, type, count(*) from entitlement_plan_items group by 1,2 having count(*) > 1. Dedupe manually — the migration won't reconcile rows for you.lang/vendor/laravel-entitlements/ is the source of truth. Anything left in root lang/ is dead weight and will silently stop overriding.filament-badgeable-column back to *. The ^4.0 pin exists because prefer-lowest builds resolved a Filament 3 release with an unregistered view namespace, which breaks table rendering at runtime, not at install time.prefer-lowest in CI is the only way that dependency bug shows up; keep one job on it, otherwise you'll ship an install that works for you and breaks for a user.afterCommit listeners may not run. If your tests assert listener behaviour, commit explicitly or assert on the resulting state (transition row, slot_used) rather than on the event.nextBillingAt() returns ends_at for non-recurring plans. Don't render a projected billing period for one-off plans; check for recurrence before displaying dates.changePlan() transactions short. It takes a lock on the anchor license. Don't wrap it in a long-running job that also does I/O — you'll serialise all plan changes for that billable, and 1.3.1's fix only prevents duplicate pending transitions, not lock contention.type_enum validation errors are now actionable — read the message rather than guessing; the usual cause is an enum that doesn't implement the expected label contract, or one whose getLabel() returns null (now tolerated, but worth auditing for null labels leaking into UI).PlanTransition event listeners now that they're guaranteed post-commit.How can I help you explore Laravel packages today?