Features

Controllable platform capabilities with Boolean, Numeric, and String value types.

Features are the atomic building blocks of the Entitlements system. Each feature represents a controllable capability — a boolean toggle, a numeric quota, or a string configuration. Features have a stable system key (Name) that never changes, making them safe to reference in code.

Feature Entity

A Feature defines a controllable platform capability. The Name field is a stable system key used in code; DisplayNameEn/DisplayNameAr are user-facing labels.

PropertyTypeDescription
IdGuidPrimary key (auto-generated)
NamestringUnique system key (e.g., 'Identity.MaxAdminsPerTenant')
DisplayNameEn / DisplayNameArstringLocalized titles for display in the UI
ModulestringWhich module owns this feature (e.g., 'Identity', 'Chat', 'ERP')
ValueTypeenumBoolean, Numeric, String
DefaultValuestringFallback value when no edition or override specifies one
Descriptionstring?Detailed description of what this feature controls
Categorystring?Grouping category (e.g., 'Security', 'Limits')
SortOrderintDisplay order in the feature list
IsVisibleInUIboolIf false, hidden from edition management UI (internal-only)
IsSystemboolSystem features are read-only (seeded, cannot be deleted via API)
IsMarketingOnlyboolIf true, shown on pricing cards but not enforced by runtime checks
CreatedAtDateTimeWhen the feature was created
UpdatedAtDateTime?Last modification timestamp
Feature EntityC#
public class Feature : AuditableEntity, ISoftDeletable
{
    public string Name { get; set; } = string.Empty;        // Unique system key
    public string DisplayName { get; set; } = string.Empty;  // UI label
    public string? Description { get; set; }
    public string? Category { get; set; }                    // Grouping
    
    public FeatureValueType ValueType { get; set; } = FeatureValueType.Boolean;
    public string DefaultValue { get; set; } = "false";     // Global fallback
    
    public bool IsSystem { get; set; }                      // Read-only if true
    public bool IsVisible { get; set; } = true;
    
    // Validation constraints (Numeric features)
    public string? MinValue { get; set; }                   // e.g. "0"
    public string? MaxValue { get; set; }                   // e.g. "1000", "-1" = unlimited
    public string? AllowedValues { get; set; }              // e.g. "light,dark,custom"
    
    public int SortOrder { get; set; }
}

Value Types

Feature values are stored as strings but interpreted according to their ValueType. The system validates values against the expected type at creation and update time.

TypeStored AsExample ValuesQuota TrackingUse Case
Boolean"true" / "false""true"NoToggle capabilities on/off (Chat.Enabled, SSO.Enabled)
NumericInteger string"50", "1000", "-1" (unlimited)Yes (QuotaCounter)Resource limits (MaxAdmins, MaxStorage, MaxApiCalls)
StringArbitrary string"dark", "premium", "custom-logo.png"NoConfiguration values (Theme, LogoUrl, SupportTier)
FeatureValueType EnumC#
public enum FeatureValueType
{
    /// <summary>On/Off toggle, stored as "true" or "false"</summary>
    Boolean = 0,
    
    /// <summary>Numeric limit, stored as integer string. -1 = unlimited</summary>
    Numeric = 1,
    
    /// <summary>Arbitrary string configuration value</summary>
    String = 2,
}

System vs Custom Features

SCRIPE distingscripehes between system features (seeded at startup, read-only) and custom features (created by admins via API):

System FeaturesCustom Features
Created ByStartup seeder (EntitlementsStartupSeeder)Platform admin via API
DeletableNo (ISoftDeletable guarded)Yes
EditableOnly DefaultValue and DescriptionFully editable
PurposeCore platform capabilitiesTenant-specific extensions
ExamplesChat.Enabled, MaxAdmins, SSO.EnabledCustomReports, MaxProjects, WhiteLabel

Feature Seeding

System features are automatically seeded at application startup by EntitlementsStartupSeeder. The seeder checks if each system feature already exists (by Name) and only creates missing ones — existing features are never overwritten.

EntitlementsStartupSeeder.csC#
public class EntitlementsStartupSeeder : IStartupSeeder
{
    private readonly IFeatureRepository _features;

    public async Task SeedAsync(CancellationToken ct)
    {
        var systemFeatures = new[]
        {
            new Feature 
            { 
                Name = "Chat.Enabled", 
                DisplayName = "Chat", 
                ValueType = FeatureValueType.Boolean,
                DefaultValue = "false",
                IsSystem = true,
                Category = "Communication"
            },
            new Feature 
            { 
                Name = "MaxAdmins", 
                DisplayName = "Maximum Administrators",
                ValueType = FeatureValueType.Numeric,
                DefaultValue = "5",
                MinValue = "1",
                MaxValue = "-1",
                IsSystem = true,
                Category = "Limits"
            },
            new Feature 
            { 
                Name = "SSO.Enabled", 
                DisplayName = "Single Sign-On",
                ValueType = FeatureValueType.Boolean,
                DefaultValue = "false",
                IsSystem = true,
                Category = "Security"
            },
        };

        foreach (var feature in systemFeatures)
        {
            if (!await _features.ExistsByNameAsync(feature.Name, ct))
                await _features.AddAsync(feature, ct);
        }
    }
}

Quota Tracking (QuotaCounter)

Numeric features support automatic quota enforcement via the QuotaCounter entity. The FeatureCheckBehavior checks the current usage against the resolved limit for every IRequireFeature command targeting a numeric feature.

PropertyTypeDescription
TenantIdGuidThe tenant owning this counter
ResourceTypestringThe feature/resource type being tracked (e.g. Identity.MaxAdminsPerTenant)
UsedintCurrent confirmed resource count in the database
ReservedintActive slot reservations for in-flight requests
MaxintSpecific tenant limit resolved from plans or settings (-1 = unlimited)
PoolRootTenantIdGuid?Root tenant ID if this resource is shared across sibling tenants in a pool
PoolMaxintMaximum total limit allowed across the entire pool (-1 = unlimited)
TenantQuotaService Gating FlowC#
// Inside TenantQuotaService.cs:
public async Task<bool> IsQuotaAvailableAsync(
    Guid tenantId, 
    string featureName, 
    int currentUsage, 
    CancellationToken ct = default)
{
    // Concurrency Protection: acquire row-level lock on the tenant
    await _tenantRepository.LockTenantForQuotaAsync(tenantId, ct);

    // Resolve: 1. IFeatureChecker limit, 2. Fallback to legacy TenantSettings, 3. -1 fallback
    var limit = await GetEffectiveLimitAsync(tenantId, featureName, ct);
    if (limit == -1) 
        return true; 
        
    return currentUsage < limit;
}

Feature Cache

Resolved feature values are cached in the IFeatureCache to avoid database queries on every request. The cache is invalidated whenever an edition's features change, a subscription is modified, or an override is set/removed. In microservice deployments without the Entitlements module, a NoOpFeatureCache treats all features as enabled.

Feature Request — IRequireFeature command
GetResolvedValue()
FeatureCache — In-memory cache
not found
Cache Miss — First access
query DB
Resolution Chain — Override → Edition → Default
Cache Hit — Instant return

IRequireFeature Pattern

To gate any CQRS command behind a feature check, simply implement the IRequireFeature marker interface. The FeatureCheckBehavior automatically intercepts the request, resolves the tenant's feature value, and rejects if disabled or over quota.

Using Feature Gates & Quota GatingC#
// ─── Boolean Feature Gate (IRequireFeature) ───
// Step 1: Mark your command
public class SendBulkEmailCommand : ICommand, IRequireFeature
{
    public string RequiredFeatureName => "BulkEmail.Enabled";
    
    public List<string> Recipients { get; set; } = [];
    public string Subject { get; set; } = string.Empty;
    public string Body { get; set; } = string.Empty;
}

// Step 2: That's it! FeatureCheckBehavior handles the rest.
// If "BulkEmail.Enabled" is resolved to false → returns 403 Forbidden
// If resolved to true → command proceeds normally to the handler


// ─── Numeric Feature Quota (Handler Enforcement) ───
// For numeric features with resource limits, validate quotas in the handler:
public class CreateAdminCommandHandler : IRequestHandler<CreateAdminCommand, Result<Guid>>
{
    private readonly ITenantQuotaService _quotaService;
    private readonly IQuotaCounterRepository _quotaCounterRepository;

    public async Task<Result<Guid>> Handle(CreateAdminCommand request, CancellationToken ct)
    {
        // 1. Validate quota availability (concurrency-safe via LockTenantForQuotaAsync)
        var isAvailable = await _quotaService.IsQuotaAvailableAsync(request.TenantId, "Identity.MaxAdminsPerTenant", currentUsage, ct);
        if (!isAvailable) 
            return Result.Failure(Error.Forbidden("Admin limit exceeded"));

        // 2. Atomically reserve slot in repository
        var reserved = await _quotaCounterRepository.TryReserveSlotAsync(request.TenantId, "Identity.MaxAdminsPerTenant", ct);
        if (!reserved) 
            return Result.Failure(Error.Forbidden("Admin limit exceeded"));

        try
        {
            // Provision resource...
            await _quotaCounterRepository.ConfirmReservationAsync(request.TenantId, "Identity.MaxAdminsPerTenant", ct);
            return Result.Success(adminId);
        }
        catch
        {
            // Rollback reservation on failure
            await _quotaCounterRepository.ReleaseReservationAsync(request.TenantId, "Identity.MaxAdminsPerTenant", ct);
            throw;
        }
    }
}

API Endpoints

The Features controller exposes 5 CRUD endpoints. System features cannot be deleted:

MethodEndpointDescriptionAuth
GET/api/v1/featuresList all features (paginated, filterable by category/type)🔒 Required
GET/api/v1/features/{id}Get feature details by ID🔒 Required
POST/api/v1/featuresCreate a new custom feature🔒 Required
PUT/api/v1/features/{id}Update feature metadata (system features: DefaultValue/Description only)🔒 Required
DELETE/api/v1/features/{id}Soft-delete a custom feature (system features cannot be deleted)🔒 Required