Skip to content

Plan Entitlements & Feature Gating

v1.2

Plan entitlements are machine-readable capabilities attached to a plan. They let you enforce feature limits (e.g. "100 API calls per month", "up to 5 team members") based on which plan a user is subscribed to — without hardcoding limits in your module code.


How entitlements work

Each PlanEntitlement row represents one capability on one plan:

ColumnDescription
plan_idThe plan this entitlement belongs to
keyMachine-readable identifier (e.g. api_calls_per_month)
valueThe limit or flag (see conventions below)
labelHuman-readable text shown on the public pricing page
is_visibleWhether to show this on the pricing page

Value conventions

Stored valueMeaningHelper
NULLUnlimited / feature is on with no cap$e->isUnlimited()true
'100'Numeric cap of 100$e->numericLimit()100
'true'Boolean feature — enabled$e->boolValue()true
'false'Boolean feature — disabled$e->boolValue()false

Seeding entitlements for your module

Seed entitlements when your module activates. Do this in a setup seeder triggered by the ModuleActivated event:

php
use App\Models\Plan;
use App\Models\PlanEntitlement;
use App\Models\Product;

// Get all plans for your product
$plans = Plan::where('product_id', $yourProductId)->get();

foreach ($plans as $plan) {
    PlanEntitlement::firstOrCreate(
        ['plan_id' => $plan->id, 'key' => 'api_calls_per_month'],
        [
            'value'      => $plan->name === 'Enterprise' ? null : '100',
            'label'      => $plan->name === 'Enterprise' ? 'Unlimited API calls' : '100 API calls / month',
            'is_visible' => true,
        ]
    );
}

Use firstOrCreate so re-running the seeder doesn't overwrite admin edits.


Reading entitlements in code

Check a single key

php
use App\Models\PlanEntitlement;

$value = PlanEntitlement::valueOf($plan, 'api_calls_per_month');
// Returns the raw string value, or null if unlimited / key doesn't exist.

Enforce a numeric limit

php
$entitlement = PlanEntitlement::where('plan_id', $plan->id)
    ->where('key', 'api_calls_per_month')
    ->first();

if ($entitlement && ! $entitlement->isUnlimited()) {
    $limit  = $entitlement->numericLimit(); // e.g. 100
    $current = $user->apiCallsThisMonth();

    if ($current >= $limit) {
        abort(429, 'Monthly API call limit reached.');
    }
}

Check a boolean feature flag

php
$entitlement = PlanEntitlement::where('plan_id', $plan->id)
    ->where('key', 'priority_support')
    ->first();

$hasPrioritySupport = $entitlement?->boolValue() ?? false;

Gate routes with middleware

Each module is responsible for registering its own middleware alias in its ModuleServiceProvider::boot() and applying it to its routes. See the AiAgentsCore module for a real example — it registers agent.access and applies it to all agent routes.

The pattern:

php
// In ModuleServiceProvider::boot()
$this->app['router']->aliasMiddleware('my-module.auth', MyModuleAuthMiddleware::class);
php
// In routes/web.php
Route::middleware(['web', 'auth', 'my-module.auth'])
    ->group(function () {
        Route::get('/account/my-feature', MyController::class);
    });

Inside the middleware, resolve the entitlement:

php
$entitlement = PlanEntitlement::where('plan_id', $plan->id)
    ->where('key', 'my_feature')
    ->first();

if (! $entitlement || $entitlement->boolValue() === false) {
    abort(403, 'Your plan does not include this feature.');
}

Displaying entitlements on pricing pages

ProductService::preparePricingData() automatically includes visible entitlements in the plan data passed to pricing page views. Each plan in $pricingData['plans'] has a features array populated from PlanEntitlement rows where is_visible = true.

No extra work needed — just seed the entitlements with is_visible = true and they appear on the pricing page.


Admin editing

Admins can edit entitlement values through the Product Manager (Admin → Products → Edit → Plans → Plan modal → Features tab). Changes take effect immediately with no deployment required.

Released under the Commercial License.