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.
| Property | Type | Description |
|---|---|---|
| Id | Guid | Primary key |
| TenantId | Guid | The tenant this override applies to |
| FeatureId | Guid | The feature being overridden |
| Value | string | Custom value (must match Feature.ValueType) |
| Reason | string? | Why this override was applied (audit trail) |
| ExpiresAt | DateTime? | Optional expiration (null = permanent) |
| IsActive | bool | Whether the override is currently active |
| AppliedBy | Guid | Admin who set this override |
| CreatedAt | DateTime | When the override was created |
| UpdatedAt | DateTime? | Last modification timestamp |
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:
Use Case Scenarios
The following real-world scenarios demonstrate when overrides provide the most value:
| Scenario | Feature | Edition Default | Override Value | Result |
|---|---|---|---|---|
| Enterprise deal with extra admins | MaxAdmins | 50 (Pro plan) | 500 | 500 admins |
| Temporary feature trial | AdvancedReporting.Enabled | false (Basic plan) | true (expires in 30 days) | true until expiry, then false |
| Custom branding for VIP | WhiteLabel.LogoUrl | null (Pro plan) | "acme-logo.png" | Custom logo used |
| Unlimited quota for partner | MaxApiCalls | 10000 (Pro plan) | "-1" (unlimited) | Unlimited API calls |
| Feature disabled for compliance | Chat.Enabled | true (Enterprise plan) | false | Chat 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.
{
"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.
{
"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).
{
"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 neededAudit 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.
| Event | Tracked Data | Purpose |
|---|---|---|
| Override Created | Who, When, Feature, Value, Reason | Know who gave custom access and why |
| Override Updated | Previous value, New value, Changed by | Track all modifications |
| Override Removed | Removed by, Removal reason | Know when custom deals end |
| Override Expired | Expiry date, Feature reverted to | Automatic lifecycle events |
API Endpoints
The TenantFeatures controller exposes 4 endpoints for managing per-tenant overrides and resolved values:
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/v1/tenants/{tenantId}/features/overrides | List all overrides for a specific tenant | 🔒 Required |
| POST | /api/v1/tenants/{tenantId}/features/overrides | Set 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/resolved | Get 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 Type | Merge Logic / Priority | Example Behavior |
|---|---|---|
| Boolean | OR logic ('true' wins) | If either subscription enables a feature, it is enabled. |
| Numeric | MAX 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. |
| String | First-wins logic (based on sorted subscription type) | Base plan values naturally override Trial, Add-on, and Free tier values (Base > Trial > AddOn > Free). |
// 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.
// 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.