Skip to content

Select language

Current language: English

User Custom Configuration Management Best Practices

Edit page
HagiCode for Windows Microsoft Store artwork
HagiCode for Windows is now on Microsoft Store
HagiCode for Windows is officially live on Microsoft Store. Windows users can install it directly from the storefront and stay on the store-managed update path. Open the listing and take a look.
Open Microsoft Store
Awesome Site banner: community-curated open-source collections
Awesome Site: Curated Open-Source Collections
Find your next tool, idea, or inspiration in community-curated open-source collections. Pick a topic, read in your language, and follow the link back to the original source.
Explore Awesome Site
Awesome Design banner: a gallery of DESIGN.md design systems
Awesome Design: A DESIGN.md Gallery
Browse curated design systems through live previews and README references, then copy a DESIGN.md into your project and let your AI agent build a page that matches.
Browse the Design Gallery
OpenSpec banner: agree on the spec, then build with AI
OpenSpec: Agree on the Plan Before Your AI Writes Code
OpenSpec is a lightweight spec layer for AI coding. Describe a change, review the proposal, specs, design, and tasks your AI drafts, then build it and archive it into living specs. No rigid phases, and it fits existing codebases and the AI assistants you already use.
Explore OpenSpec
OmniRoute Docs banner: one endpoint for many AI providers
OmniRoute Docs: One Endpoint for Many AI Providers
Learn how OmniRoute routes many AI providers and models through a single endpoint, with free-tier support and automatic fallback so your coding tools keep working.
Read the OmniRoute Docs

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:

  1. Empty value fallback: Give empty values a reasonable default, can’t just leave them empty
  2. Alias mapping: Common aliases, abbreviations, uniformly convert to standard format
  3. Dialect normalization: Dialect variants merge into standard codes, after all writing code isn’t doing dialect research
  4. 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:

  1. Add property to backend DTO
public class FrontendConfigGeneralSettingsDto
{
// ... existing properties
public string? NewFeatureEnabled { get; set; }
}
  1. Add normalization logic
private static FrontendConfigGeneralSettingsDto NormalizeGeneralSettings(
FrontendConfigGeneralSettingsDto settings)
{
return new FrontendConfigGeneralSettingsDto
{
// ... existing properties
NewFeatureEnabled = NormalizeOptionalString(settings.NewFeatureEnabled),
};
}
  1. 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>
  1. Update configuration group definition (if adding new group)
export const ALL_PERSISTABLE_FRONTEND_CONFIG_GROUPS = [
// ... existing groups
'newFeatureSettings',
] as const;

Common Issue Handling

  1. Configuration loss: Backup and recovery mechanisms, better safe than sorry
  2. Concurrency conflicts: Optimistic locking or pessimistic locking, like SemaphoreSlim
  3. Performance issues: Support partial updates, don’t move the entire configuration object around every time
  4. 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

If this article helps you:

开始使用 HagiCode

一次安装,几分钟上手

HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。