Subscriptionify: Feature-Based Subscription Management for Laravel

Published on by

Subscriptionify: Feature-Based Subscription Management for Laravel image

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 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 schedule
use 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.

Yannick Lyn Fatt photo

Staff Writer at Laravel News and Full stack web developer.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Tinkerwell

Enjoy coding and debugging in an editor designed for fast feedback and quick iterations. It's like a shell for your application – but with multi-line editing, code completion, and more.

Visit Tinkerwell
Harpoon: Next generation time tracking and invoicing logo

Harpoon: Next generation time tracking and invoicing

The next generation time-tracking and billing software that helps your agency plan and forecast a profitable future.

Harpoon: Next generation time tracking and invoicing
Laravel Cloud logo

Laravel Cloud

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Cloud
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Acquaint Softtech logo

Acquaint Softtech

Acquaint Softtech offers AI-ready Laravel developers who onboard in 48 hours at $3000/Month with no lengthy sales process and a 100 percent money-back guarantee.

Acquaint Softtech
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $9500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
PhpStorm logo

PhpStorm

The go-to PHP IDE with extensive out-of-the-box support for Laravel and its ecosystem.

PhpStorm

The latest

View all →
Toolkit: Reusable AI Tools for the Laravel AI SDK image

Toolkit: Reusable AI Tools for the Laravel AI SDK

Read article
Laracon US 2026 Reveals Its Full Speaker Lineup image

Laracon US 2026 Reveals Its Full Speaker Lineup

Read article
The State of PHP 2026 Survey Is Now Open image

The State of PHP 2026 Survey Is Now Open

Read article
Version-Controlled Documentation Inside Your Laravel App with Laradocs image

Version-Controlled Documentation Inside Your Laravel App with Laradocs

Read article
Typed Translation Accessors in Laravel 13.15.0 image

Typed Translation Accessors in Laravel 13.15.0

Read article
Refresh Your Laravel Database Without Dropping Every Table image

Refresh Your Laravel Database Without Dropping Every Table

Read article