The settings feature is a database-backed application configuration system for values that should be changeable at runtime without a redeploy. In the current starter implementation, it supports:
- system-scoped settings in the admin UI
- organization-scoped settings in the admin UI
- user-scoped settings in runtime code
- in-memory cached reads
- immediate cache invalidation on writes
- masking of sensitive values in admin list views
- startup seeding of settings already used by runtime services
This is not a mirror of appsettings.json. It is a separate database-backed configuration layer.
This starter includes a settings system so application behavior can be adjusted at runtime and optionally vary by scope.
Typical use cases include:
- storage upload limits and policies
- appearance preferences
- tenant or organization branding
- future feature/module configuration that should not require a deployment
The settings system is already used by runtime services.
FileService reads settings in category Storage for:
AllowedContentTypesMaxFileSizeBytesSoftDelete
These affect upload validation and deletion behavior.
ThemeService reads and writes:
Appearance.Theme
This is the current user’s theme preference, resolved through the settings service.
The settings feature is split across the persisted data model, runtime resolution service, admin CRUD service, and admin UI.
| Area | Responsibility | Main files |
|---|---|---|
| Data model | Stores scoped settings rows | src/Server/Database/Core/Models/Setting.cs |
| Query layer | Filters and searches setting records | src/Server/Database/Core/Data/Queries/BasicsImplementation/CoreSettingsQuery.cs |
| Decorators | Maps DB rows to admin DTOs | src/Server/Database/Core/Data/Decorators/SettingDecorators.cs |
| Runtime service | Resolves effective values for application code | src/Server/Admin/Services/Settings/SettingsService.cs |
| Cache invalidation | Invalidates cached runtime lookups after writes | src/Server/Admin/Services/Settings/ISettingsCacheInvalidator.cs |
| Admin CRUD service | Lists and mutates DB setting rows | src/Server/Admin/Services/Settings/SettingsAdminService.cs |
| Admin UI | Displays and edits settings | src/Server/Admin/WebService/UI/Pages/Admin/SettingsPage.razor |
| Seeder | Creates default settings already used by runtime features | src/Server/Admin/Services/Seeding/SettingsDataSeeder.cs |
The persisted record is Setting.
public class Setting : DatabaseBaseObject
{
public Guid? TenantId { get; set; }
public Guid? OrganizationId { get; set; }
public Guid? UserId { get; set; }
public string Category { get; set; } = string.Empty;
public string Key { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public string ValueType { get; set; } = "string";
public string? Description { get; set; }
public bool IsSensitive { get; set; }
}| Field | Meaning |
|---|---|
Category |
Logical grouping such as Storage or Appearance |
Key |
Specific setting name inside the category |
Value |
Stored raw/JSON value |
ValueType |
Intended type hint such as string, int, bool, or json |
IsSensitive |
Whether admin views should mask the value |
TenantId / OrganizationId / UserId |
Scope markers |
The schema allows:
- system scope
- tenant scope
- organization scope
- user scope
But the current implementation does not expose all of those equally. The schema is broader than the admin surface.
The runtime service currently resolves settings in this effective order:
- user
- organization
- system
Tenant rows exist in the data model, but runtime tenant context resolution is not currently wired up.
The admin settings page currently exposes only:
SystemOrganization
The tenant tab was removed because it was present in the UI before the actual tenant-resolution behavior was implemented.
User-scoped settings are still supported by runtime code through SettingsService.SetAsync and GetAsync, which is how theme preference works today.
They are just not managed from the admin settings page.
SettingsService is the main runtime API.
| Method | Purpose |
|---|---|
GetAsync<T>(category, key) |
Resolve one setting for the current runtime context |
SetAsync<T>(category, key, value) |
Write at the most specific available runtime scope |
SetAsync<T>(..., tenantId, organizationId, userId) |
Explicit scoped write |
GetAllForCategoryAsync(category) |
Resolve all keys in one category for the current context |
The service resolves context from:
- the current authenticated user
IOperationContext.PrimaryOrganizationId
Tenant context is currently left null.
Runtime reads are cached in memory for 10 minutes.
- cache key includes category, key, and resolved scope context
- cache invalidation uses a versioned key prefix
- writes increment the cache version for that category/key
This means admin writes now become visible immediately to runtime readers without needing to enumerate all possible scope-specific cache entries.
The admin page is:
/admin/settings
The page now requires:
Admin.Settings.View
Mutating actions additionally require:
Admin.Settings.Edit
That means read-only settings access is now possible without automatically granting edit/delete capability.
The page supports:
- switching between system and organization scopes
- searching by category, key, or description
- creating settings
- editing setting values
- deleting settings
- masking sensitive values in the list
Sensitive values are masked in list views. The admin page does not preload the raw secret into the visible list response.
An important fix was made here: saving a sensitive setting while its value is masked no longer overwrites the stored value with an empty string.
SettingsDataSeeder now seeds settings that are already consumed by runtime services so the page is not empty on a fresh database.
| Category | Key | Value | Type | Used by |
|---|---|---|---|---|
Storage |
AllowedContentTypes |
* |
string |
FileService |
Storage |
MaxFileSizeBytes |
10485760 |
int |
FileService |
Storage |
SoftDelete |
false |
bool |
FileService |
Appearance |
Theme |
System |
string |
ThemeService |
Before this seeder existed, the settings page could be empty even though runtime services were already reading from the settings system and falling back to hardcoded defaults. Seeding makes the feature visible and editable immediately.
This settings system is separate from application configuration in appsettings.json.
- app startup wiring
- infrastructure configuration
- provider registration
- deployment/environment configuration
Examples:
- database provider
- connection strings
- SMTP provider registration values
- background task settings
- health endpoint protection settings
- runtime-adjustable business or feature behavior
- scoped overrides
- values that an admin may reasonably change after deployment
Examples:
- file upload policy
- per-user theme preference
- future org-scoped branding or module toggles
The service supports these common type hints:
stringintbooljson
The stored Value is still essentially a serialized string payload, and deserialization depends on the requested generic type in GetAsync<T>.
For simple cases:
- strings are unwrapped
- ints are parsed
- bools are parsed
- other types fall back to JSON deserialization
The most important current limits are:
The schema supports tenant-scoped records, but the current runtime context does not resolve a tenant id and the admin page does not expose tenant settings.
The masking behavior is appropriate for the admin list view, but this is not a full secrets-management system with rotation, audit-specific redaction workflows, or secure reveal flows.
The admin page does not manage user-scoped rows directly.
The cache invalidation fix handles this process correctly, but if you later run multiple app instances and need cross-node cache coherence, this will need to evolve beyond simple in-memory cache versioning.
Use the settings system for:
- business or feature values that should change without redeploying
- values that may vary by organization or user
- values where a database-backed admin workflow makes sense
Do not use it as the first choice for:
- low-level infrastructure wiring
- secrets that need a real secret manager
- large structured configuration blobs unless you truly need them
The settings feature is a real runtime configuration layer, not just an admin stub. Today it is primarily useful for:
- system-scoped application settings
- organization-scoped overrides
- user-scoped preferences managed by runtime services
After the recent fixes, it is in a much better state:
- sensitive values are safer to edit
- runtime cache invalidation is immediate
- the admin permission boundary is clearer
- the page now shows seeded settings that are actually used by the application
The main caveat is that tenant support remains schema-level groundwork rather than a finished end-to-end feature.