Feature Overrides

Per-tenant feature value customization that bypasses edition defaults.

Feature Overrides allow platform administrators to customize feature values for individual tenants, regardless of their subscribed edition. Overrides take the highest priority in the resolution chain, making them perfect for custom sales deals, special promotions, or one-off exceptions.

Override Entity

A TenantFeatureOverride sets a custom value for a specific feature on a specific tenant. It includes an optional Reason field for audit purposes.

PropertyTypeDescription
IdGuidPrimary key
TenantIdGuidThe tenant this override applies to
FeatureIdGuidThe feature being overridden
ValuestringCustom value (must match Feature.ValueType)
Reasonstring?Why this override was applied (audit trail)
ExpiresAtDateTime?Optional expiration (null = permanent)
IsActiveboolWhether the override is currently active
AppliedByGuidAdmin who set this override
CreatedAtDateTimeWhen the override was created
UpdatedAtDateTime?Last modification timestamp
TenantFeatureOverride EntityC#
public class TenantFeatureOverride : AuditableEntity
{
    public Guid TenantId { get; set; }
    
    public Guid FeatureId { get; set; }
    public Feature Feature { get; set; } = null!;
    
    public string Value { get; set; } = string.Empty;
    // Must match Feature.ValueType:
    //   Boolean → "true" / "false"
    //   Numeric → integer string (e.g. "500")
    //   String  → arbitrary string
    
    public string? Reason { get; set; }                // Audit trail
    public DateTime? ExpiresAt { get; set; }           // null = permanent
    public bool IsActive { get; set; } = true;
    public Guid AppliedBy { get; set; }                // Who set it
}

Resolution Priority

Overrides sit at the top of the resolution chain. When the system resolves a feature value for a tenant, it checks for an override first:

Feature Check — FeatureCheckBehavior triggered
highest priority
1. Check Override — TenantFeatureOverride exists + active + not expired?
not found
2. Check Edition — EditionFeature value for tenant's subscribed edition?
not found
3. Use Default — Feature.DefaultValue (global fallback)
always exists
Resolved Value — Applied to the request

Use Case Scenarios

The following real-world scenarios demonstrate when overrides provide the most value:

ScenarioFeatureEdition DefaultOverride ValueResult
Enterprise deal with extra adminsMaxAdmins50 (Pro plan)500500 admins
Temporary feature trialAdvancedReporting.Enabledfalse (Basic plan)true (expires in 30 days)true until expiry, then false
Custom branding for VIPWhiteLabel.LogoUrlnull (Pro plan)"acme-logo.png"Custom logo used
Unlimited quota for partnerMaxApiCalls10000 (Pro plan)"-1" (unlimited)Unlimited API calls
Feature disabled for complianceChat.Enabledtrue (Enterprise plan)falseChat disabled for this tenant

Setting an Override

To set an override, POST to the tenant features endpoint with the feature ID, custom value, and an optional reason for audit purposes.

POST /api/v1/tenants/{tenantId}/features/overridesJSON
{
  "featureId": "550e8400-e29b-41d4-a716-446655440000",
  "value": "500",
  "reason": "Enterprise deal — 500 admins for annual contract",
  "expiresAt": null
}

// Response 200:
{
  "id": "770e8400-e29b-41d4-a716-446655440099",
  "tenantId": "660e8400-e29b-41d4-a716-446655440001",
  "featureId": "550e8400-e29b-41d4-a716-446655440000",
  "featureName": "MaxAdmins",
  "value": "500",
  "previousValue": "50",
  "source": "Override",
  "reason": "Enterprise deal — 500 admins for annual contract",
  "expiresAt": null,
  "isActive": true,
  "appliedBy": "admin-user-id",
  "createdAt": "2026-03-02T10:00:00Z"
}

Resolved Features Endpoint

The GET /api/v1/tenants/{tenantId}/features/resolved endpoint returns the final, effective value for every feature for a given tenant. It shows the resolution source (Override, Edition, or Default) for each entry, making it easy to debug and audit.

GET /api/v1/tenants/{tenantId}/features/resolvedJSON
{
  "tenantId": "660e8400-e29b-41d4-a716-446655440001",
  "editionName": "Pro",
  "resolvedFeatures": [
    {
      "featureId": "...",
      "name": "Chat.Enabled",
      "displayName": "Chat",
      "valueType": "Boolean",
      "value": "true",
      "source": "Edition",
      "editionDefault": "true",
      "overrideValue": null,
      "featureDefault": "false"
    },
    {
      "featureId": "...",
      "name": "MaxAdmins",
      "displayName": "Maximum Administrators",
      "valueType": "Numeric",
      "value": "500",
      "source": "Override",
      "editionDefault": "50",
      "overrideValue": "500",
      "featureDefault": "5",
      "currentUsage": 12,
      "overrideReason": "Enterprise deal",
      "overrideExpiresAt": null
    },
    {
      "featureId": "...",
      "name": "SSO.Enabled",
      "displayName": "Single Sign-On",
      "valueType": "Boolean",
      "value": "false",
      "source": "Default",
      "editionDefault": null,
      "overrideValue": null,
      "featureDefault": "false"
    }
  ]
}

Expiring Overrides

Overrides can have an optional ExpiresAt date. When the expiration date passes, the override is automatically deactivated and the feature falls back to the edition value (or global default).

Temporary Override (30-day trial)JSON
{
  "featureId": "...")
  "value": "true",
  "reason": "30-day trial of Advanced Reporting feature",
  "expiresAt": "2026-04-02T00:00:00Z"
}

// After expiry:
// - Override is automatically marked isActive = false
// - Feature falls back to edition value (or default)
// - No manual intervention needed

Audit Trail

Every override operation is tracked with full audit information. The Reason field on each override provides context for why the custom value was applied.

EventTracked DataPurpose
Override CreatedWho, When, Feature, Value, ReasonKnow who gave custom access and why
Override UpdatedPrevious value, New value, Changed byTrack all modifications
Override RemovedRemoved by, Removal reasonKnow when custom deals end
Override ExpiredExpiry date, Feature reverted toAutomatic lifecycle events

API Endpoints

The TenantFeatures controller exposes 4 endpoints for managing per-tenant overrides and resolved values:

MethodEndpointDescriptionAuth
GET/api/v1/tenants/{tenantId}/features/overridesList all overrides for a specific tenant🔒 Required
POST/api/v1/tenants/{tenantId}/features/overridesSet or update a feature override for a tenant🔒 Required
DELETE/api/v1/tenants/{tenantId}/features/overrides/{id}Remove (deactivate) a feature override🔒 Required
GET/api/v1/tenants/{tenantId}/features/resolvedGet all resolved feature values for a tenant (shows source: Override/Edition/Default)🔒 Required

Subscription Merging & Sorting

When a tenant has multiple active subscriptions (such as a base plan and an add-on), features are merged. Subscriptions are loaded and sorted by their type ascending: Lifetime (0) → Monthly (1) → Yearly (2) → Trial (3) → AddOn (4) → Free (5).

Value TypeMerge Logic / PriorityExample Behavior
BooleanOR logic ('true' wins)If either subscription enables a feature, it is enabled.
NumericMAX logic (highest wins, -1 is unlimited)If Pro gives 50 and Add-on gives 100, the limit is 100. If either is -1, it is unlimited.
StringFirst-wins logic (based on sorted subscription type)Base plan values naturally override Trial, Add-on, and Free tier values (Base > Trial > AddOn > Free).
Subscription SortingC#
// Inside SubscriptionRepository.cs:
// Active subscriptions are ordered by type priority:
return await _db.Subscriptions
    .Where(s => s.TenantId == tenantId && s.Status == SubscriptionStatus.Active)
    .OrderBy(s => s.Type) // Lifetime (0) → Monthly (1) → Yearly (2) → Trial (3) → AddOn (4) → Free (5)
    .ToListAsync(ct);

Permission Auto-Population

When a subscription becomes active or trialing, a SubscriptionChangedEvent is published. This is handled by SubscriptionChangedEventHandler in the Identity module to automatically synchronize the tenant's permissions pool and super admin role permissions.

SubscriptionChangedEventHandler.csC#
// Inside SubscriptionChangedEventHandler.cs:
public async Task Handle(SubscriptionChangedEvent notification, CancellationToken ct)
{
    var tenantId = notification.TenantId;
    
    if (notification.IsRevocation) // Canceled, Expired, Suspended, or PendingPayment
    {
        // Revoke all permissions and lock super admin role
        await _tenantPermissionManager.SyncPermissionsForModulesAsync(tenantId, [], ct);
        await _permissionCache.InvalidateAll();
        return;
    }

    // Active/Trialing: Extract modules from enabled features (e.g. "Communication.Enabled" -> "Communication")
    var enabledModules = notification.Features
        .Where(f => f.Name.EndsWith(".Enabled") && f.Value == "true")
        .Select(f => f.Name.Split('.')[0])
        .ToList();

    // Sync tenant permission pool
    await _tenantPermissionManager.SyncPermissionsForModulesAsync(tenantId, enabledModules, ct);

    // Sync super admin role permissions
    await _roleManager.SyncSuperAdminRolePermissionsAsync(tenantId, ct);
    
    // Invalidate cached permissions
    await _permissionCache.InvalidateAll();
}

Best Practices

Follow these guidelines to keep your override system maintainable and auditable.