User Custom Configuration Management Best Practices





User Custom Configuration Management Best Practices
Nothing much really, just sharing a configuration management solution. From architecture to practice, well, hope it helps you avoid some pitfalls.
Background
Configuration management—important, yes, but also trivial. It’s like those little habits in life—some people like morning coffee, some like late-night coding. These seemingly trivial preferences quietly define who you are. User custom configuration is the same—theme switching, language selection, keyboard shortcut customization. When these features are done well, users feel the product “gets them,” and stickiness naturally increases.
But building a complete system isn’t as simple as imagined. Version management, backward compatibility, data validation, concurrency control, DLC feature gating—these problems emerge one after another, like teenage troubles, unavoidable. During HagiCode development, we were tortured by these problems too, but eventually figured out some tricks.
About HagiCode
This solution, well, is what we explored in HagiCode. HagiCode is an AI code assistant with many features—interface language, AI language preference, theme, voice recognition, notifications, quick actions. There’s quite a lot to configure. Precisely because users want so much, we had to build this system.
Project URL: github.com/HagiCode-org/site
Architecture Design
Layered Architecture
HagiCode’s configuration management system, well, it’s just divided into several layers, each with its own responsibility:
┌─────────────────────────────────────────┐│ Frontend (React + Redux) ││ - Configuration state management ││ - UI form rendering ││ - Group-based configuration persistence│└─────────────────────────────────────────┘ ↓ HTTP/REST API┌─────────────────────────────────────────┐│ Application Service Layer ││ - FrontendConfigAppService ││ - Business logic processing ││ - Permission control │└─────────────────────────────────────────┘ ↓┌─────────────────────────────────────────┐│ Domain Layer (Config Store) ││ - FrontendConfigStore ││ - Configuration read/write ││ - Data validation and normalization │└─────────────────────────────────────────┘ ↓┌─────────────────────────────────────────┐│ Infrastructure Layer ││ - YAML file storage ││ - ISystemManagedVaultService ││ - Concurrency control (SemaphoreSlim) │└─────────────────────────────────────────┘The benefits of this layered approach are actually quite obvious:
- Clear responsibilities: Each layer manages its own business without interfering
- Easy testing: Each layer can be tested independently, changes are safe
- Flexible extension: Want to change storage method or modify API, other layers still work
Core Interface Design
The backend configuration storage interface is roughly defined like this:
public interface IFrontendConfigStore{ // Get user's complete configuration Task<FrontendConfigStoreResult> GetAsync(CancellationToken cancellationToken = default);
// Update configuration (supports partial updates) Task<FrontendConfigStoreResult> UpdateAsync( UpdateFrontendConfigRequestDto input, CancellationToken cancellationToken = default);
// AI language state management Task<FrontendConfigAiLanguageState> GetAiLanguageStateAsync( string userId, CancellationToken cancellationToken = default);
Task<FrontendConfigAiLanguageState> SetAiLanguageAsync( string userId, string language, CancellationToken cancellationToken = default);}The key points of this interface are actually quite understandable:
- Async operations: Everything is async, after all nobody wants to wait
- Cancellation tokens: Timeout if operations take too long, don’t just hang
- Partial updates: Only update what needs changing, no need to翻 the entire configuration
Configuration Data Structure
Configuration Grouping Design
HagiCode groups configurations by function, each group can be updated independently without interference:
public class FrontendConfigSnapshotDto{ // General settings public FrontendConfigGeneralSettingsDto GeneralSettings { get; set; }
// AI language configuration public FrontendConfigAILanguageDto AiLanguage { get; set; }
// Project scope public FrontendConfigProjectScopeDto ProjectScope { get; set; }
// Interface language public string UiLanguage { get; set; }
// Theme public string Theme { get; set; }
// Voice recognition public FrontendConfigVoiceRecognitionDto VoiceRecognition { get; set; }
// Notification settings public FrontendConfigNotificationsDto Notifications { get; set; }
// Session sorting public FrontendConfigSessionSortingDto SessionSorting { get; set; }
// Quick actions public FrontendConfigQuickActionsDto QuickActions { get; set; }
// Confirmation dialogs public FrontendConfigConfirmDialogDto ConfirmDialog { get; set; }
// Session presets public FrontendConfigSessionPresetsDto SessionPresets { get; set; }
// Project icon configuration public FrontendConfigProjectIconConfigDto ProjectIconConfig { get; set; }
// Common comments public FrontendConfigCommonCommentsDto CommonComments { get; set; }}Configuration grouping is like categorizing life’s trivialities—work is work, entertainment is entertainment, relationships are relationships. Mixed together is messy, clearly categorized is relaxed:
- Update on demand: Update what you change, no need to affect everything
- Permission control: Different configurations can have different permissions, after all not everyone can mess around
- DLC gating: Advanced features can be bound to DLC, want to use good stuff就得付费嘛
Frontend Configuration Grouping Definition
On the frontend, all persistable configuration groups are listed:
export const ALL_PERSISTABLE_FRONTEND_CONFIG_GROUPS = [ 'generalSettings', 'aiLanguage', 'projectScope', 'uiLanguage', 'theme', 'voiceRecognition', 'notifications', 'sessionSorting', 'quickActions', 'confirmDialog', 'sessionPresets', 'projectIconConfig', 'commonComments',] as const;
export type PersistableFrontendConfigGroup = typeof ALL_PERSISTABLE_FRONTEND_CONFIG_GROUPS[number];Data Validation and Normalization
Language Normalization Example
Configuration normalization, simply put, is keeping data consistent. Taking language configuration as an example:
public static class FrontendConfigLanguageRules{ // Supported interface language list public static readonly HashSet<string> ValidUiLanguages = new(StringComparer.OrdinalIgnoreCase) { "zh-CN", "zh-Hant", "en-US", "ja-JP", "ko-KR", "de-DE", "fr-FR", "es-ES", "pt-BR", "ru-RU" };
public static string NormalizeUiLanguage(string? value) { // Empty value handling: return default language if (string.IsNullOrWhiteSpace(value)) return "en-US";
var normalized = value.Trim();
// Alias handling: convert common aliases to standard codes normalized = normalized.ToLower() switch { "zh" or "chinese" or "cn" => "zh-CN", "en" or "english" => "en-US", "ja" or "japanese" => "ja-JP", "ko" or "korean" => "ko-KR", _ => normalized };
// Dialect variant handling if (normalized.StartsWith("zh-Hans", StringComparison.OrdinalIgnoreCase)) return "zh-CN"; if (normalized.StartsWith("zh-TW", StringComparison.OrdinalIgnoreCase)) return "zh-Hant";
// Verify and return return ValidUiLanguages.Contains(normalized) ? normalized : "en-US"; }}Normalization is like tidying a room—when things get messy you need to organize, otherwise even you can’t find things:
- Empty value fallback: Give empty values a reasonable default, can’t just leave them empty
- Alias mapping: Common aliases, abbreviations, uniformly convert to standard format
- Dialect normalization: Dialect variants merge into standard codes, after all writing code isn’t doing dialect research
- Final verification: Ensure returned value is in valid list, otherwise all that work is wasted
Configuration Version Management
Version numbers are for backward compatibility—old users’ data shouldn’t be lost due to version upgrades:
public const string CurrentSchemaVersion = "1.0";
private static FrontendConfigGeneralSettingsDto NormalizeGeneralSettings( FrontendConfigGeneralSettingsDto settings){ return new FrontendConfigGeneralSettingsDto { // Ensure version is latest Version = settings.Version > 0 ? Math.Max(settings.Version, 37) : 37,
// Handle default values for newly added fields NewFeatureEnabled = settings.NewFeatureEnabled ?? true,
// ... other fields };}DLC Feature Gating
HagiCode supports DLC feature switches—some advanced configuration items require purchasing DLC to use. This is common in commercial software—basic features are free, want to use good stuff就得掏钱, after all developers need to eat too.
DLC Access Check
private async Task<PreparedFrontendConfigUpdate> PrepareUpdateAsync( UpdateFrontendConfigRequestDto input, FrontendConfigStoreResult current, CancellationToken cancellationToken){ // Check DLC access permissions var accessState = await _grainFactory .GetDlcAccessStateGrain(TurboEngineDlcId) .GetAccessStateAsync();
var blockedFields = new List<string>();
if (!accessState.IsActive) { // DLC not activated, preserve current configuration if (input.GeneralSettings?.BrandingLogo != null) { blockedFields.Add("brandingLogo"); // Preserve old value input.GeneralSettings.BrandingLogo = current.Snapshot.GeneralSettings.BrandingLogo; }
if (input.GeneralSettings?.BrandingTitle != null) { blockedFields.Add("brandingTitle"); input.GeneralSettings.BrandingTitle = current.Snapshot.GeneralSettings.BrandingTitle; } }
return new PreparedFrontendConfigUpdate( input, new FrontendConfigUpdateDiagnosticsDto { Status = blockedFields.Count > 0 ? "partially-applied" : "success", BlockedFields = blockedFields, });}Diagnostic Information Display
The frontend tells users through diagnostic information which configurations were blocked or modified—got to let people know what happened:
{hasPartialSaveWarning ? ( <Alert data-testid="general-settings-partial-save-alert"> <AlertTitle>Settings Save Restricted by DLC</AlertTitle> <AlertDescription> {hasBlockedBranding && ( <p>Brand customization changes skipped. Install or enable {dlcName} to save Logo and title updates.</p> )} {hasNormalizedTheme && ( <p>Selected document theme unavailable, saved as basic theme.</p> )} </AlertDescription> </Alert>) : null}Frontend State Management
Redux Slice
The frontend uses Redux to manage configuration state, quite conventional actually:
export const frontendConfigSlice = createSlice({ name: 'frontendConfig', initialState, reducers: { setConfigStatus(state, action: PayloadAction<FrontendConfigStatus>) { state.status = action.payload; }, updateConfigGroups(state, action: PayloadAction<Partial<FrontendConfigSnapshot>>) { // Merge configuration updates Object.assign(state.snapshot, action.payload); }, }, extraReducers: (builder) => { builder .addCase(fetchConfig.pending, (state) => { state.status = 'loading'; }) .addCase(fetchConfig.fulfilled, (state, action) => { state.status = 'succeeded'; state.snapshot = action.payload; }) .addCase(fetchConfig.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }); },});Configuration Service Encapsulation
export const frontendConfigService = { getConfig(): Promise<FrontendConfigResponse> { return createRequest<FrontendConfigResponse>({ method: 'GET', url: '/api/frontend-config', }); },
updateConfig(requestBody: UpdateFrontendConfigRequest): Promise<FrontendConfigResponse> { return createRequest<FrontendConfigResponse>({ method: 'PUT', url: '/api/frontend-config', body: requestBody, mediaType: 'application/json', }); },
// Persist configuration by group persistConfigGroup( group: PersistableFrontendConfigGroup, value: unknown ): Promise<FrontendConfigResponse> { return this.updateConfig({ configGroup: group, value: value, }); },};Concurrency Control
Configuration update operations must be thread-safe, otherwise if two people change configuration at the same time, whose gets saved? HagiCode uses SemaphoreSlim for concurrency control, well, it’s a common trick:
private readonly SemaphoreSlim _semaphore = new(1, 1);
public async Task<FrontendConfigStoreResult> UpdateAsync( UpdateFrontendConfigRequestDto input, CancellationToken cancellationToken = default){ await _semaphore.WaitAsync(cancellationToken); try { // Read current configuration var current = await GetAsync(cancellationToken);
// Prepare update var prepared = await PrepareUpdateAsync(input, current, cancellationToken);
// Write configuration await WriteConfigAsync(prepared.UpdatedConfig, cancellationToken);
return new FrontendConfigStoreResult(prepared.UpdatedConfig, prepared.Diagnostics); } finally { _semaphore.Release(); }}Practice Guide
Steps to Add New Configuration Items
Want to add new configuration items? Follow this order:
- Add property to backend DTO
public class FrontendConfigGeneralSettingsDto{ // ... existing properties public string? NewFeatureEnabled { get; set; }}- Add normalization logic
private static FrontendConfigGeneralSettingsDto NormalizeGeneralSettings( FrontendConfigGeneralSettingsDto settings){ return new FrontendConfigGeneralSettingsDto { // ... existing properties NewFeatureEnabled = NormalizeOptionalString(settings.NewFeatureEnabled), };}- Add form control on frontend
<SettingsCard icon={<Star className="h-5 w-5" />} title="New Feature Settings" description="Control new feature enablement status"> <NewFeatureToggle /></SettingsCard>- Update configuration group definition (if adding new group)
export const ALL_PERSISTABLE_FRONTEND_CONFIG_GROUPS = [ // ... existing groups 'newFeatureSettings',] as const;Common Issue Handling
- Configuration loss: Backup and recovery mechanisms, better safe than sorry
- Concurrency conflicts: Optimistic locking or pessimistic locking, like SemaphoreSlim
- Performance issues: Support partial updates, don’t move the entire configuration object around every time
- Security: Sensitive configurations (like API keys) must be encrypted, otherwise if stolen it’s trouble
Summary
A complete configuration management system, well, requires considering quite a lot—data structure, validation normalization, version management, DLC gating, concurrency control, none can be missing. HagiCode’s solution runs quite stably in production, at least能满足 complex configuration management requirements.
This article is written here,也算是 sharing some of my experience. Good configuration management not only provides good user experience, but also reduces maintenance costs and makes product iteration more worry-free.
References
- HagiCode project URL: github.com/HagiCode-org/site
- HagiCode official site: hagicode.com
- One-click installation experience: docs.hagicode.com/installation/docker-compose
- Desktop desktop quick installation: hagicode.com/desktop/
- Watch official version demo video: www.bilibili.com/video/BV1z4oWB3EpY/
If this article helps you:
- Come to GitHub and give a Star: github.com/HagiCode-org/site
- Visit official site to learn more: hagicode.com
- Public beta has started, welcome to play
开始使用 HagiCode
一次安装,几分钟上手
HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。
Ecosystem Sites
Quick Links
Community
© 2026 HagiCode | Powered By hagilight-starlight@0.6.1