Entitlements Overview
Edition-based feature gating with Features, Editions, Subscriptions, and per-tenant Overrides.
The Entitlements module is SCRIPE's plan-and-feature management engine. It defines what capabilities each tenant gets, how plans (editions) bundle those capabilities, and how subscriptions link tenants to plans.
| Current Reality | Status | Boundary |
|---|---|---|
| SCRIPE platform features, editions, tenant subscriptions, and quotas | Current | Keep in Entitlements |
| Platform invoices and payment transactions | Current | Platform billing only: SCRIPE billing tenants |
| Platform leads and lead activities | Fenced | Move to Platform Sales or CRM when that module exists |
| Tenant plans, user subscriptions, and tenant payment gateways | Fenced | Do not treat as final Catalog or Finance Lite architecture |
| Tenant customer invoices, receipts, refunds, balances, and taxes | Missing | Build later in Finance Lite, not by reusing platform billing entities |
What is Entitlements?
Entitlements is the module responsible for controlling which features a tenant can access based on their subscribed edition (plan). It provides a three-level resolution chain: Feature defaults → Edition values → Per-tenant overrides, ensuring maximum flexibility for both platform operators and reseller tenants.
| Concept | Analogy | Example |
|---|---|---|
| Feature | A switch or dial on your platform | Chat.Enabled, MaxAdmins, Theme |
| Edition | A product SKU / pricing plan | Basic ($29/mo), Pro ($99/mo), Enterprise (custom) |
| Subscription | A customer's contract | Acme Corp → Pro plan, monthly, active since Jan 2026 |
| Override | A one-off exception | Give Acme Corp 500 admins instead of the Pro default of 50 |
Architecture
The Entitlements system is composed of four interconnected domains that work together to provide a complete feature-gating solution.
Four Domains
Each domain handles a specific aspect of the entitlements lifecycle:
| Domain | Entity | Responsibility | Key Operations |
|---|---|---|---|
| Features | Feature | Define controllable capabilities (boolean toggle, numeric quota, string config) | CRUD, Seed system features, ValueType validation |
| Editions | Edition, EditionVersion, EditionFeature | Named plans that bundle feature values with versioning | CRUD, Version management, Rollout strategies, Direct-apply |
| Subscriptions | TenantSubscription | Bind tenants to editions with full lifecycle | Assign, Upgrade, Downgrade, Suspend, Resume, Cancel, Renew |
| Overrides | TenantFeatureOverride | Per-tenant custom values bypassing edition defaults | Set, Remove, List, Resolve all features |
Feature Value Resolution Chain
When the system needs to determine a feature value for a tenant, it follows a strict priority chain. The highest-priority source that provides a value wins.
Priority (highest → lowest):
┌─────────────────────────────────────────────────────────────────────────┐
│ 1. TenantFeatureOverride → Custom value set for THIS specific tenant │
│ Example: "MaxAdmins" = 500 (override for Acme Corp) │
├─────────────────────────────────────────────────────────────────────────┤
│ 2. EditionFeature → Value set in the tenant's active edition │
│ Example: "MaxAdmins" = 50 (Pro plan default) │
├─────────────────────────────────────────────────────────────────────────┤
│ 3. Feature.DefaultValue → Global fallback for the feature │
│ Example: "MaxAdmins" = 5 (platform default) │
└─────────────────────────────────────────────────────────────────────────┘
Resolution Order:
Check Override → exists? use it : Check Edition → exists? use it : use DefaultPipeline Integration
SCRIPE integrates entitlements directly into the SCRIPE mediator CQRS pipeline via FeatureCheckBehavior. Commands and queries that implement IRequireFeature are automatically gated — if the tenant's resolved feature value is disabled, the request is rejected before reaching the handler.
// Mark a command to require a feature
public class CreateChatRoomCommand : ICommand<Guid>, IRequireFeature
{
public string RequiredFeatureName => "Chat.Enabled";
// ... command properties
public string Name { get; set; }
public string Description { get; set; }
}Quota Gating & Slot Reservations
Numeric features represent quotas that are enforced when creating tenant resources. SCRIPE uses a concurrency-safe, atomic reservation pattern to manage these limits.
// Inside QuotaCounterRepository.cs:
public async Task<bool> TryReserveSlotAsync(Guid tenantId, string resourceType, CancellationToken ct = default)
{
var counter = await GetAsync(tenantId, resourceType, ct);
if (counter == null) return true; // No counter = no limit
// Check 1: Per-tenant limit
if (counter.Max != -1 && (counter.Used + counter.Reserved) >= counter.Max)
return false;
// Check 2: Pooled limit (sibling tenants sharing a PoolRootTenantId)
if (counter.PoolRootTenantId.HasValue)
{
var rootCounter = await GetAsync(counter.PoolRootTenantId.Value, resourceType, ct);
if (rootCounter != null && rootCounter.PoolMax != -1)
{
var poolUsage = await GetPoolUsageAsync(counter.PoolRootTenantId.Value, resourceType, ct);
if (poolUsage >= rootCounter.PoolMax)
return false;
}
}
// Atomically reserve the slot
counter.Reserved++;
await _db.SaveChangesAsync(ct);
return true;
}CQRS Command & Query Map
The Entitlements module registers 31 SCRIPE request handlers spanning the four domains. Each command has a corresponding FluentValidation validator for input validation.
| Domain | Commands | Queries |
|---|---|---|
| Features | CreateFeature, UpdateFeature, DeleteFeature | GetFeatures (paginated), GetFeatureById |
| Editions | CreateEdition, UpdateEdition, DeleteEdition, SetEditionFeatures, DirectApplyFeatures, CreateEditionVersion, PublishEditionVersion, RollbackEditionVersion | GetEditions, GetEditionById, GetEditionFeatures, GetEditionVersions |
| Subscriptions | AssignSubscription, UpgradeSubscription, DowngradeSubscription, SuspendSubscription, ResumeSubscription, CancelSubscription, RenewSubscription | GetSubscriptions, GetSubscriptionById, GetDowngradeImpact, GetTenantActiveSubscription |
| Overrides | SetFeatureOverride, RemoveFeatureOverride | GetTenantOverrides, GetResolvedFeatures |
Dependency Injection Registration
All Entitlements services are registered via the AddEntitlementsModule extension method in DependencyInjection.cs. The module follows SCRIPE's standard registration pattern.
public static class DependencyInjection
{
public static IServiceCollection AddEntitlementsModule(
this IServiceCollection services, IConfiguration config)
{
// Domain repositories
services.AddScoped<IFeatureRepository, FeatureRepository>();
services.AddScoped<IEditionRepository, EditionRepository>();
services.AddScoped<ISubscriptionRepository, SubscriptionRepository>();
services.AddScoped<IFeatureOverrideRepository, FeatureOverrideRepository>();
// Application services
services.AddScoped<IFeatureCache, FeatureCache>();
services.AddScoped<IEditionConstraintValidator, EditionConstraintValidator>();
services.AddScoped<IDowngradeImpactValidator, DowngradeImpactValidator>();
services.AddScoped<IOverflowPolicyExecutor, OverflowPolicyExecutor>();
services.AddScoped<IQuotaCounterProvisioner, QuotaCounterProvisioner>();
// AstraFlow mediator pipeline behavior
services.AddTransient(typeof(IPipelineBehavior<,>),
typeof(FeatureCheckBehavior<,>));
// Startup seeder (seeds system features)
services.AddTransient<IStartupSeeder, EntitlementsStartupSeeder>();
return services;
}
}Backend Structure
The Entitlements backend follows SCRIPE's standard Clean Architecture module layout with Domain, Application, and Infrastructure layers.
Entitlements/
├── Entitlements.Domain/
│ ├── Entities/
│ │ ├── Feature.cs # Controllable capability
│ │ ├── Edition.cs # Named plan (Basic, Pro, Enterprise)
│ │ ├── EditionFeature.cs # Feature value within an edition
│ │ ├── EditionVersion.cs # Versioned snapshot of edition features
│ │ ├── TenantSubscription.cs # Tenant ↔ Edition binding
│ │ ├── TenantFeatureOverride.cs # Per-tenant custom value
│ │ └── QuotaCounter.cs # Usage tracking for numeric features
│ ├── Interfaces/
│ │ ├── IFeatureRepository.cs
│ │ ├── IEditionRepository.cs
│ │ ├── ISubscriptionRepository.cs
│ │ └── IFeatureOverrideRepository.cs
│ ├── Enums/
│ │ ├── FeatureValueType.cs # Boolean, Numeric, String
│ │ ├── OverflowPolicy.cs # Block, Warn, Allow
│ │ ├── SubscriptionType.cs # Monthly, Annual, Lifetime, Trial
│ │ ├── SubscriptionStatus.cs # Active, Suspended, Cancelled, Expired
│ │ ├── RolloutStrategy.cs # Immediate, Gradual, Manual
│ │ └── ExpiryBehavior.cs # Downgrade, Suspend, Grace
│ └── Specifications/
│ └── FeatureSearchSpec.cs
├── Entitlements.Application/
│ ├── Commands/
│ │ ├── Features/ # CreateFeature, UpdateFeature, DeleteFeature
│ │ ├── Editions/ # CRUD + SetFeatures + DirectApply + Versions
│ │ ├── Subscriptions/ # Assign, Upgrade, Downgrade, Suspend, Resume, Cancel
│ │ └── Overrides/ # SetOverride, RemoveOverride
│ ├── Queries/
│ │ ├── Features/ # GetFeatures, GetFeatureById
│ │ ├── Editions/ # GetEditions, GetById, GetFeatures, GetVersions
│ │ ├── Subscriptions/ # GetSubscriptions, GetById, GetDowngradeImpact
│ │ └── Overrides/ # GetOverrides, GetResolvedFeatures
│ ├── DTOs/ # Request/Response DTOs
│ ├── Mapping/ # explicit DTO mapping rules
│ ├── Services/
│ │ └── EditionConstraintValidator.cs
│ └── Abstractions/
│ ├── IFeatureCache.cs
│ ├── IDowngradeImpactValidator.cs
│ ├── IOverflowPolicyExecutor.cs
│ └── IQuotaCounterProvisioner.cs
└── Entitlements.Infrastructure/
├── EntitlementsDbContextFactory.cs
├── EntitlementsStartupSeeder.cs
├── Persistence/
│ ├── FeatureRepository.cs
│ ├── EditionRepository.cs
│ ├── SubscriptionRepository.cs
│ └── FeatureOverrideRepository.cs
├── Caching/
│ ├── FeatureCache.cs
│ └── NoOpFeatureCache.cs
└── DependencyInjection.csFrontend Structure
The frontend mirrors the backend with four sub-modules (editions, features, subscriptions, overrides), each following the SOLID View/ViewModel pattern.
src/modules/entitlements/
├── editions/ # Edition management
│ └── src/
│ ├── domain/
│ │ ├── entities/ # Edition.ts, EditionFeature.ts, EditionVersion.ts
│ │ └── interfaces/ # IEditionRepository.ts
│ ├── data/
│ │ ├── models/ # EditionDto.ts, EditionFeatureDto.ts
│ │ ├── mappers/ # EditionMapper.ts
│ │ └── repositories/ # EditionRepository.ts
│ └── presentation/
│ ├── viewmodels/ # useEditionsViewModel.ts, useEditionDetailViewModel.ts
│ ├── views/ # EditionsView.tsx, EditionDetailView.tsx
│ └── components/ # EditionFeaturesTable.tsx, VersionHistory.tsx
├── features/ # Feature registry
│ └── src/ { domain, data, presentation }
├── subscriptions/ # Subscription lifecycle
│ └── src/ { domain, data, presentation }
└── overrides/ # Per-tenant overrides
└── src/ { domain, data, presentation }API Controllers
The Entitlements module exposes 31 API endpoints across 4 controllers, all authenticated with JWT and protected by permission-based authorization.
| Controller | Route Prefix | Endpoints | Permission Prefix | Key Operations |
|---|---|---|---|---|
| EditionsController | /api/v1/editions | 11 | editions.* | CRUD, SetFeatures, DirectApply, Versions, Publish, Rollback |
| FeaturesController | /api/v1/features | 5 | All Feature Permissions | CRUD (system features are read-only) |
| SubscriptionsController | /api/v1/subscriptions | 12 | subscriptions.* | Assign, Upgrade, Downgrade, Suspend, Resume, Cancel, Renew, DowngradeImpact |
| TenantFeaturesController | /api/v1/tenants/{id}/features | 4 | All Feature Permissions | SetOverride, RemoveOverride, GetOverrides, GetResolved |
With vs Without Entitlements
The following table shows the difference in capabilities when the Entitlements module is enabled versus running without it:
| Capability | Without Entitlements | With Entitlements |
|---|---|---|
| Feature Gating | Manual if/else checks scattered in code | Automatic pipeline-level gating via IRequireFeature |
| Plan Management | Hard-coded tier logic | Dynamic editions with feature bundles |
| Quota Enforcement | No enforcement | Automatic quota tracking with QuotaCounter |
| Plan Changes | Manual DB updates | Safe upgrade/downgrade with impact analysis |
| Custom Deals | Code changes required | Override via API without touching code |
| Version Control | No versioning | Edition versions with rollout strategies |
| Audit Trail | No tracking | Every change audited automatically |
| Reseller Support | Not possible | Tenant-scoped retail editions |
NoOp Fallback
When the Entitlements module is not loaded (e.g. in a microservice that doesn't include Entitlements), SCRIPE registers a NoOpFeatureCache. This allows IRequireFeature commands to pass through without errors — all features are treated as enabled by default.
/// <summary>
/// Registered when the Entitlements module is not loaded.
/// All features are treated as enabled with unlimited quotas.
/// </summary>
public class NoOpFeatureCache : IFeatureCache
{
public Task<ResolvedFeature> GetResolvedValue(
Guid tenantId, string featureName)
{
return Task.FromResult(new ResolvedFeature
{
Name = featureName,
Value = "true", // Always enabled
ValueType = FeatureValueType.Boolean,
Source = ResolutionSource.Default,
});
}
public Task<int> GetCurrentUsage(Guid tenantId, string featureName)
=> Task.FromResult(0); // No usage tracked
}Getting Started
Follow these 5 steps to set up the Entitlements system for your platform. Each step builds on the previous one:
Step 1: Define Features
→ POST /api/v1/features
→ Create features like "Chat.Enabled" (Boolean), "MaxAdmins" (Numeric)
Step 2: Create Editions
→ POST /api/v1/editions
→ Create plans like "Basic", "Pro", "Enterprise"
→ Set feature values per edition
Step 3: Assign Subscriptions
→ POST /api/v1/subscriptions
→ Link each tenant to an edition
Step 4: Gate Commands (Optional)
→ Implement IRequireFeature on commands
→ FeatureCheckBehavior automatically enforces
Step 5: Apply Overrides (Optional)
→ POST /api/v1/tenants/{id}/features/overrides
→ Give specific tenants custom values