interactively/plans
A Laravel package for creating and managing subscription plans and features. Define pricing tiers, billing intervals, and feature limits, then attach plans to models to power SaaS-style subscriptions and access control.
interactively/plans is a Laravel subscription/entitlement package: you define plans, attach features and limits, subscribe users (or other billable models), check feature access, and generate invoices. Treat it as the catalog and entitlement layer; payment collection usually stays with Stripe/Cashier/Paddle or your own billing flow.
Minimal setup:
Install and publish assets:
composer require interactively/plans
php artisan vendor:publish --provider="Interactively\Plans\PlansServiceProvider"
php artisan migrate
Publish both config and migrations; check the published config/plans.php for table names and model bindings before migrating.
Add the billable trait to your user model:
use Interactively\Plans\Traits\HasPlans;
class User extends Authenticatable
{
use HasPlans;
}
Seed a first plan and feature:
use Interactively\Plans\Models\Plan;
use Interactively\Plans\Models\Feature;
$pro = Plan::create([
'name' => 'Pro',
'slug' => 'pro',
'price' => 1900,
'interval' => 'month',
]);
$apiCalls = Feature::create([
'name' => 'API calls',
'slug' => 'api-calls',
'type' => 'limit', // limit / feature / consumable, depending on version
]);
$pro->features()->attach($apiCalls, ['value' => 10000]);
First use case — subscribe and gate a feature:
$user->subscribeTo($pro);
if ($user->subscription()->canUseFeature('api-calls')) {
// allow access
}
Where to look first: the published config, the HasPlans trait, the Plan/Feature/Subscription models, and the migrations. Those define the real API surface faster than the README.
Model plans as a catalog, not as business logic. Keep plan definitions in seeders or an admin CRUD, and keep feature slugs stable. Renaming a slug breaks every canUseFeature() call in the codebase.
Use feature types deliberately. Typical patterns:
feature / boolean: on/off entitlements such as “SSO” or “priority support”.limit: numeric caps such as seats or projects.consumable: metered usage that resets each billing period.Store the per-plan value on the pivot (plan_feature.value) rather than hardcoding limits in plan columns.
Centralize entitlement checks. Wrap the package calls in a policy, middleware, or dedicated service so controllers do not scatter canUseFeature() everywhere:
class EnsurePlanFeature
{
public function handle($request, Closure $next, string $feature)
{
abort_unless(
$request->user()->subscription()?->canUseFeature($feature),
403
);
return $next($request);
}
}
Meter consumption inside transactions. For consumable features, record usage in a DB transaction and re-read the subscription state before writing:
DB::transaction(function () use ($user) {
$subscription = $user->subscription()->lockForUpdate();
abort_unless($subscription->canUseFeature('api-calls'), 429);
$subscription->recordFeatureUsage('api-calls', 1);
});
This avoids the classic read-check-write race where two requests both see one unit remaining.
Handle plan changes as explicit workflows. Upgrades, downgrades, and cancellations usually need more than swapping a foreign key: decide whether usage resets, whether the period end is preserved, and whether invoices are prorated. Put that behind a ChangePlan action class so the policy is testable.
Use events for side effects. Listen for subscription created/renewed/canceled/expired events to provision resources, send receipts, or revoke access. Do not put mail sending or external API calls directly in controllers around subscribeTo().
Generate invoices from a listener, not from HTTP requests. On renewal or subscription creation, create the invoice in a queued listener, assign a sequential number, persist line items, and store the PDF on a private disk. Send the invoice notification separately so a mail failure does not roll back billing state.
Integrate with a payment provider at the edges. Keep provider IDs on your subscription/invoice records if the package exposes metadata or a JSON column. Webhooks should be idempotent: look up by provider event ID or subscription ID before applying changes.
Test with time control and factories. Use Carbon::setTestNow() to test period boundaries, renewals, and expirations. Create plans/features in test setup rather than relying on seeded production data.
composer.json. Review the issue tracker and recent commits.migrate, and confirm the table names in config match what your code queries.null value, and 0 can mean different things across versions. Normalize this in one helper so “not included” does not silently become “unlimited”.canUseFeature() is tempting for hot paths, but invalidate on usage consumption, plan change, cancellation, and renewal. A stale “allowed” result after downgrade is a business risk.$user->subscription(), its features, and the pivot value directly. Most “feature not working” reports are a missing pivot row, a slug typo, or an expired subscription rather than a package bug.How can I help you explore Laravel packages today?