Subscriptionify is a Laravel package, built by Rasel Islam Rafi, for modelling subscription plans and the features they unlock. Where Laravel Cashier wraps a payment provider's billing API, Subscriptionify stays gateway-agnostic: it tracks plans, feature quotas, and usage in your own database, leaving the question of who collects the money to you. That makes it a fit for applications that bill through a provider Cashier doesn't cover, charge from a prepaid balance, or grant access without charging at all. It requires PHP 8.2 and supports Laravel 11, 12, and 13.
Installation publishes a config file and migrations that create six tables for plans, features, the plan/feature pivot, subscriptions, usage records, and direct feature grants:
composer require revoltify/subscriptionify php artisan vendor:publish --tag=subscriptionify-configphp artisan vendor:publish --tag=subscriptionify-migrationsphp artisan migrate
Any model can become billable by implementing the Subscribable contract and adding the InteractsWithSubscriptions trait, so you can attach subscriptions to a User, an Organisation, or a Workspace, depending on how your application is structured:
use Revoltify\Subscriptionify\Concerns\InteractsWithSubscriptions;use Revoltify\Subscriptionify\Contracts\Subscribable; class Workspace extends Model implements Subscribable{ use InteractsWithSubscriptions;}
Four feature types for different quota patterns
The core idea is that not every feature behaves the same way. A plan might gate access to a capability, meter a depletable monthly allowance, or cap a running total. Subscriptionify models these as four distinct feature types, each with its own consumption rules:
- Toggle is a plain on/off gate, used for capabilities a plan either includes or doesn't.
- Consumable is a depletable quota that resets on a schedule, such as a monthly allowance of API calls.
- Limit is a hard cap on a running total that can be freed again, such as active projects or seats, where deleting one frees a slot.
- Metered tracks pay-per-use consumption with no cap, charging per unit.
Features are created once, then attached to plans through a pivot that carries the allocation for that plan:
use Revoltify\Subscriptionify\Models\Feature;use Revoltify\Subscriptionify\Enums\FeatureType; $reports = Feature::create([ 'name' => 'Exported Reports', 'slug' => 'reports', 'type' => FeatureType::Consumable,]); $plan->features()->attach($reports, [ 'value' => 500, // 0 means unlimited 'unit_price' => '0.02000000', // price per unit once the quota is exceeded 'reset_period' => 1, 'reset_interval' => 'month',]);
Usage tracking on the subscribable model
Once a model is subscribed to, the usage methods live directly on it. You check access, test whether a number of units is available, and record consumption without reaching into the subscription or pivot records yourself:
$workspace->subscribe($plan); $workspace->hasFeature('reports'); // is the feature available at all?$workspace->canConsume('reports', 10); // are 10 units available right now?$workspace->consume('reports', 10); // record usage, throws if the quota is exceeded$workspace->tryConsume('reports', 10); // same, but returns false instead of throwing$workspace->remainingUsage('reports'); // units left in the current period
For Limit features, release() hands units back to free a slot, which is what separates a limit from a consumable that only counts down:
$workspace->consume('projects', 1); // creating a project$workspace->release('projects', 1); // deleting it frees the slot
Direct grants independent of the plan
Plans aren't the only way to allocate features. grantFeature() assigns a feature directly to a subscribable and grants it on top of whatever the plan provides. If the plan includes 500 reports and you grant another 1,000, the available quota becomes 1,500. This covers one-off top-ups, promotional bonuses, and per-customer adjustments without creating a bespoke plan for each case:
$workspace->grantFeature('reports', value: 1_000); // With auto-reset on its own scheduleuse Revoltify\Subscriptionify\Enums\Interval; $workspace->grantFeature('reports', value: 100, resetPeriod: 1, resetInterval: Interval::Month); // Remove the grant; the plan's allocation still applies$workspace->revokeFeature('reports');
Optional overage and metered billing
Billing is opt-in. Implement the HasFunds contract alongside Subscribable, and the package begins charging against a balance you control. With HasFunds in place, consumable and limit features charge overage once their quota is exhausted (provided a unit_price is set), and metered features charge per unit from the first use:
use Revoltify\Subscriptionify\Contracts\HasFunds; class Workspace extends Model implements Subscribable, HasFunds{ use InteractsWithSubscriptions; public function getBalance(): string { return $this->balance; } public function hasSufficientFunds(string $amount): bool { return bccomp($this->balance, $amount, 8) >= 0; } public function deductFunds(string $amount, string $description): void { $this->update(['balance' => bcsub($this->balance, $amount, 8)]); }}
Because amounts are passed as strings and compared with bccomp, the math is performed in arbitrary-precision rather than floating-point. Without HasFunds, the same features fall back to hard limits that throw when exceeded, so you can ship quota enforcement first and add billing later without rewriting your consumption code.
Gating access with middleware and Blade
For protecting routes, three middleware aliases are registered and return a 403 on failure:
Route::middleware('subscribed')->group(function () { // any active or trialling subscription}); Route::middleware('plan:pro')->group(function () { // a specific plan}); Route::middleware('feature:reports')->group(function () { // a specific feature});
The matching Blade directives gate content in views, including states for trials, free plans, and the post-cancellation grace period:
@feature('custom-branding') {{-- shown only when the feature is available --}}@endfeature @onTrial {{-- trial countdown banner --}}@endonTrial
By default, both resolve the subscribable from auth()->user(), which you can change with Subscriptionify::resolveSubscribableUsing() if your billable model isn't the authenticated user.
Lifecycle, events, and scheduled expiry
Subscriptions carry the usual lifecycle methods (changePlan(), renew(), cancel(), cancelNow(), and resume() during the grace period), and each transition dispatches an event such as SubscriptionCreated, SubscriptionCancelled, or FeatureConsumed for your own listeners to act on. An included artisan command transitions active subscriptions past their end date to expired:
use Illuminate\Support\Facades\Schedule; Schedule::command('subscriptionify:expire-overdue')->hourly();
To read the full feature list, configuration options, and customisation hooks, visit the package on GitHub.