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 RealityStatusBoundary
SCRIPE platform features, editions, tenant subscriptions, and quotasCurrentKeep in Entitlements
Platform invoices and payment transactionsCurrentPlatform billing only: SCRIPE billing tenants
Platform leads and lead activitiesFencedMove to Platform Sales or CRM when that module exists
Tenant plans, user subscriptions, and tenant payment gatewaysFencedDo not treat as final Catalog or Finance Lite architecture
Tenant customer invoices, receipts, refunds, balances, and taxesMissingBuild 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.

ConceptAnalogyExample
FeatureA switch or dial on your platformChat.Enabled, MaxAdmins, Theme
EditionA product SKU / pricing planBasic ($29/mo), Pro ($99/mo), Enterprise (custom)
SubscriptionA customer's contractAcme Corp → Pro plan, monthly, active since Jan 2026
OverrideA one-off exceptionGive 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.

Features — Boolean / Numeric / String capabilities
Overrides — Per-tenant custom values
Editions — Named plans (Basic, Pro, Enterprise)
linked via
Subscriptions — Tenant ↔ Edition binding
resolved into
FeatureCache — In-memory resolved values
checked by
FeatureCheckBehavior — AstraFlow mediator pipeline gate

Four Domains

Each domain handles a specific aspect of the entitlements lifecycle:

DomainEntityResponsibilityKey Operations
FeaturesFeatureDefine controllable capabilities (boolean toggle, numeric quota, string config)CRUD, Seed system features, ValueType validation
EditionsEdition, EditionVersion, EditionFeatureNamed plans that bundle feature values with versioningCRUD, Version management, Rollout strategies, Direct-apply
SubscriptionsTenantSubscriptionBind tenants to editions with full lifecycleAssign, Upgrade, Downgrade, Suspend, Resume, Cancel, Renew
OverridesTenantFeatureOverridePer-tenant custom values bypassing edition defaultsSet, 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.

Feature Value Resolution ChainText
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 Default

Pipeline 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.

IRequireFeature InterfaceC#
// 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; }
}
Request Received — Command/Query enters FeatureCheckBehavior
Opt-in Check — Does the request implement IRequireFeature?
Yes
System Bypass — Is user system protected admin or super admin with no TenantId context?
No
Tenant Context check — Is TenantId present in current user context?
TenantId exists
Resolve Feature — IFeatureChecker.IsEnabledAsync(tenantId, featureName) called
Disabled (false)
Forbidden Failure — Return Result.Failure(Forbidden) with feature.notAvailable
Proceed — Forward to next behavior or command handler

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.

TryReserveSlotAsync ImplementationC#
// 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.

DomainCommandsQueries
FeaturesCreateFeature, UpdateFeature, DeleteFeatureGetFeatures (paginated), GetFeatureById
EditionsCreateEdition, UpdateEdition, DeleteEdition, SetEditionFeatures, DirectApplyFeatures, CreateEditionVersion, PublishEditionVersion, RollbackEditionVersionGetEditions, GetEditionById, GetEditionFeatures, GetEditionVersions
SubscriptionsAssignSubscription, UpgradeSubscription, DowngradeSubscription, SuspendSubscription, ResumeSubscription, CancelSubscription, RenewSubscriptionGetSubscriptions, GetSubscriptionById, GetDowngradeImpact, GetTenantActiveSubscription
OverridesSetFeatureOverride, RemoveFeatureOverrideGetTenantOverrides, 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.

DependencyInjection.csC#
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.

Backend Module StructureText
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.cs

Frontend Structure

The frontend mirrors the backend with four sub-modules (editions, features, subscriptions, overrides), each following the SOLID View/ViewModel pattern.

Frontend Module StructureText
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.

ControllerRoute PrefixEndpointsPermission PrefixKey Operations
EditionsController/api/v1/editions11editions.*CRUD, SetFeatures, DirectApply, Versions, Publish, Rollback
FeaturesController/api/v1/features5All Feature PermissionsCRUD (system features are read-only)
SubscriptionsController/api/v1/subscriptions12subscriptions.*Assign, Upgrade, Downgrade, Suspend, Resume, Cancel, Renew, DowngradeImpact
TenantFeaturesController/api/v1/tenants/{id}/features4All Feature PermissionsSetOverride, 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:

CapabilityWithout EntitlementsWith Entitlements
Feature GatingManual if/else checks scattered in codeAutomatic pipeline-level gating via IRequireFeature
Plan ManagementHard-coded tier logicDynamic editions with feature bundles
Quota EnforcementNo enforcementAutomatic quota tracking with QuotaCounter
Plan ChangesManual DB updatesSafe upgrade/downgrade with impact analysis
Custom DealsCode changes requiredOverride via API without touching code
Version ControlNo versioningEdition versions with rollout strategies
Audit TrailNo trackingEvery change audited automatically
Reseller SupportNot possibleTenant-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.

NoOpFeatureCache.csC#
/// <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:

Quick Start StepsText
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