Entitlement System¶
The entitlement system controls feature access, usage limits, and billing integration for workspaces and namespaces.
Quick Start¶
Check Feature Access¶
use Core\Tenant\Services\EntitlementService;
$entitlements = app(EntitlementService::class);
// Check if workspace can use a feature
$result = $entitlements->can($workspace, 'ai.credits', quantity: 5);
if ($result->isAllowed()) {
// Perform action
$entitlements->recordUsage($workspace, 'ai.credits', quantity: 5, user: $user);
} else {
// Handle denial
return response()->json([
'error' => $result->reason,
'limit' => $result->limit,
'used' => $result->used,
], 403);
}
Via Workspace Model¶
$result = $workspace->can('social.accounts');
if ($result->isAllowed()) {
$workspace->recordUsage('social.accounts');
}
Concepts¶
Features¶
Features are defined in the entitlement_features table:
| Field | Description |
|---|---|
code |
Unique identifier (e.g., ai.credits, social.accounts) |
type |
boolean, limit, or unlimited |
reset_type |
none, monthly, or rolling |
rolling_window_days |
Days for rolling window |
parent_feature_id |
For hierarchical features (pool sharing) |
Feature Types:
| Type | Behaviour |
|---|---|
boolean |
Binary on/off access |
limit |
Numeric limit with usage tracking |
unlimited |
Feature enabled with no limits |
Reset Types:
| Type | Behaviour |
|---|---|
none |
Usage accumulates forever |
monthly |
Resets at billing cycle start |
rolling |
Rolling window (e.g., last 30 days) |
Packages¶
Packages bundle features with specific limits:
// Example package definition
$package = Package::create([
'code' => 'creator',
'name' => 'Creator Plan',
'is_base_package' => true,
'monthly_price' => 19.99,
]);
// Attach features
$package->features()->attach($aiCreditsFeature->id, ['limit_value' => 100]);
$package->features()->attach($socialAccountsFeature->id, ['limit_value' => 5]);
Workspace Packages¶
Packages are provisioned to workspaces:
$workspacePackage = $entitlements->provisionPackage($workspace, 'creator', [
'source' => EntitlementLog::SOURCE_BLESTA,
'billing_cycle_anchor' => now(),
'blesta_service_id' => 'srv_12345',
]);
Statuses:
- active - Package is in use
- suspended - Temporarily disabled (e.g., payment failed)
- cancelled - Permanently ended
- expired - Past expiry date
Boosts¶
Boosts provide temporary limit increases:
$boost = $entitlements->provisionBoost($workspace, 'ai.credits', [
'boost_type' => Boost::BOOST_TYPE_ADD_LIMIT,
'limit_value' => 50,
'duration_type' => Boost::DURATION_CYCLE_BOUND,
]);
Boost Types:
| Type | Effect |
|---|---|
add_limit |
Adds to package limit |
enable |
Enables boolean feature |
unlimited |
Makes feature unlimited |
Duration Types:
| Type | Expiry |
|---|---|
cycle_bound |
Expires at billing cycle end |
duration |
Expires after set expires_at |
permanent |
Never expires |
API Reference¶
EntitlementService¶
can()¶
Check if a workspace can use a feature:
public function can(
Workspace $workspace,
string $featureCode,
int $quantity = 1
): EntitlementResult
Returns EntitlementResult with:
- isAllowed(): bool
- isDenied(): bool
- isUnlimited(): bool
- limit: ?int
- used: int
- remaining: ?int
- reason: ?string
- featureCode: string
- getUsagePercentage(): ?float
- isNearLimit(): bool (>80%)
- isAtLimit(): bool (100%)
canForNamespace()¶
Check entitlement for a namespace with cascade:
public function canForNamespace(
Namespace_ $namespace,
string $featureCode,
int $quantity = 1
): EntitlementResult
Cascade order:
1. Namespace-level packages
2. Workspace pool (if namespace->workspace_id set)
3. User tier (if namespace owned by user)
recordUsage()¶
Record feature usage:
public function recordUsage(
Workspace $workspace,
string $featureCode,
int $quantity = 1,
?User $user = null,
?array $metadata = null
): UsageRecord
provisionPackage()¶
Assign a package to a workspace:
public function provisionPackage(
Workspace $workspace,
string $packageCode,
array $options = []
): WorkspacePackage
Options:
- source - system, blesta, admin, user
- billing_cycle_anchor - Start of billing cycle
- expires_at - Package expiry date
- blesta_service_id - External billing reference
- metadata - Additional data
provisionBoost()¶
Add a temporary boost:
public function provisionBoost(
Workspace $workspace,
string $featureCode,
array $options = []
): Boost
Options:
- boost_type - add_limit, enable, unlimited
- duration_type - cycle_bound, duration, permanent
- limit_value - Amount to add (for add_limit)
- expires_at - Expiry date (for duration)
suspendWorkspace() / reactivateWorkspace()¶
Manage workspace package status:
$entitlements->suspendWorkspace($workspace, 'blesta');
$entitlements->reactivateWorkspace($workspace, 'admin');
getUsageSummary()¶
Get all feature usage for a workspace:
$summary = $entitlements->getUsageSummary($workspace);
// Returns Collection grouped by category:
// [
// 'ai' => [
// ['code' => 'ai.credits', 'limit' => 100, 'used' => 50, ...],
// ],
// 'social' => [
// ['code' => 'social.accounts', 'limit' => 5, 'used' => 3, ...],
// ],
// ]
Namespace-Level Entitlements¶
For products that operate at namespace level:
$result = $entitlements->canForNamespace($namespace, 'bio.pages');
if ($result->isAllowed()) {
$entitlements->recordNamespaceUsage($namespace, 'bio.pages', user: $user);
}
// Provision namespace-specific package
$entitlements->provisionNamespacePackage($namespace, 'bio-pro');
Usage Alerts¶
The UsageAlertService monitors usage and sends notifications:
// Check single workspace
$alerts = app(UsageAlertService::class)->checkWorkspace($workspace);
// Check all workspaces (scheduled command)
php artisan tenant:check-usage-alerts
Alert Thresholds: - 80% - Warning - 90% - Critical - 100% - Limit reached
Notification Channels:
- Email to workspace owner
- Webhook events (limit_warning, limit_reached)
Billing Integration¶
Blesta API¶
External endpoints for billing system integration:
POST /api/entitlements - Provision package
POST /api/entitlements/{id}/suspend - Suspend
POST /api/entitlements/{id}/unsuspend - Reactivate
POST /api/entitlements/{id}/cancel - Cancel
POST /api/entitlements/{id}/renew - Renew
GET /api/entitlements/{id} - Get details
Cross-App API¶
For other Host UK services to check entitlements:
GET /api/entitlements/check - Check feature access
POST /api/entitlements/usage - Record usage
GET /api/entitlements/summary - Get usage summary
Webhooks¶
Subscribe to entitlement events:
$webhookService = app(EntitlementWebhookService::class);
$webhook = $webhookService->register($workspace,
name: 'Usage Alerts',
url: 'https://api.example.com/hooks/entitlements',
events: ['limit_warning', 'limit_reached']
);
Available Events:
- limit_warning - 80%/90% threshold
- limit_reached - 100% threshold
- package_changed - Package add/change/remove
- boost_activated - New boost
- boost_expired - Boost expired
Payload Format:
{
"event": "limit_warning",
"data": {
"workspace_id": 123,
"feature_code": "ai.credits",
"threshold": 80,
"used": 80,
"limit": 100
},
"timestamp": "2026-01-29T12:00:00Z"
}
Verification:
$isValid = $webhookService->verifySignature(
$payload,
$request->header('X-Signature'),
$webhook->secret
);
Best Practices¶
Check Before Action¶
Always check entitlements before performing the action:
// Bad: Check after action
$account = SocialAccount::create([...]);
if (!$workspace->can('social.accounts')->isAllowed()) {
$account->delete();
throw new \Exception('Limit exceeded');
}
// Good: Check before action
$result = $workspace->can('social.accounts');
if ($result->isDenied()) {
throw new EntitlementException($result->reason);
}
$account = SocialAccount::create([...]);
$workspace->recordUsage('social.accounts');
Use Transactions¶
For atomic check-and-record:
DB::transaction(function () use ($workspace, $user) {
$result = $workspace->can('ai.credits', 10);
if ($result->isDenied()) {
throw new EntitlementException($result->reason);
}
// Perform AI generation
$output = $aiService->generate($prompt);
// Record usage
$workspace->recordUsage('ai.credits', 10, $user, [
'model' => 'claude-3',
'tokens' => 1500,
]);
return $output;
});
Cache Considerations¶
Entitlement checks are cached for 5 minutes. For real-time accuracy:
// Force cache refresh
$entitlements->invalidateCache($workspace);
$result = $entitlements->can($workspace, 'feature');
Feature Code Conventions¶
Use dot notation for feature codes:
Examples:
- ai.credits
- social.accounts
- social.posts.scheduled
- bio.pages
- analytics.websites
Hierarchical Features¶
For shared pools, use parent features:
// Parent feature (pool)
$aiCredits = Feature::create([
'code' => 'ai.credits',
'type' => Feature::TYPE_LIMIT,
]);
// Child feature (uses parent pool)
$aiGeneration = Feature::create([
'code' => 'ai.generation',
'parent_feature_id' => $aiCredits->id,
]);
// Both check against ai.credits pool
$workspace->can('ai.generation'); // Uses ai.credits limit