Home / Laravel / Subscriptionify: Feature-Based Subscription Management for Laravel

Subscriptionify: Feature-Based Subscription Management for Laravel

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-config
php artisan vendor:publish --tag=subscriptionify-migrations
php 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 RevoltifySubscriptionifyConcernsInteractsWithSubscriptions;
use RevoltifySubscriptionifyContractsSubscribable;
 
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 RevoltifySubscriptionifyModelsFeature;
use RevoltifySubscriptionifyEnumsFeatureType;
 
$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 schedule
use RevoltifySubscriptionifyEnumsInterval;
 
$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 RevoltifySubscriptionifyContractsHasFunds;
 
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 IlluminateSupportFacadesSchedule;
 
Schedule::command('subscriptionify:expire-overdue')->hourly();

To read the full feature list, configuration options, and customisation hooks, visit the package on GitHub.

Source: https://laravel-news.com

Tagged:

Leave a Reply

Your email address will not be published. Required fields are marked *