Adding a new setting is now simple - just 3 steps:
Edit internal/config/settings_schema.json:
"your_setting_key": {
"type": "bool", // or "int", "string"
"default": false,
"category": "general",
"encrypted": false,
"frontend_key": "your_setting_key" // snake_case (same as key)
}go run tools/settings-generator/main.goAdd to your settings component:
<SettingItem :title="t('yourSettingKey')">
<Toggle
:model-value="settings.your_setting_key"
@update:model-value="updateSetting('your_setting_key', $event)"
/>
</SettingItem>That's it! See Complete Example for a detailed walkthrough.
The settings system has been optimized to use schema-driven code generation. Instead of manually editing 11+ files, you now only need to edit 1 file and run the code generator.
Old Way (Deprecated):
- Edit 11 files manually
- ~50-100 lines of repetitive code
- High chance of copy-paste errors
- 30-45 minutes of work
New Way (Current):
- Edit 1 file (5 lines)
- Run 1 command
- Add UI and translations (optional)
- 10-15 minutes of work
Result: ~90% reduction in development time, near-zero error risk
After running the generator, these files are automatically created/updated:
- ✅
internal/config/config.go- Go struct andGetString()function - ✅
internal/config/settings_keys.go- Settings keys array for DB init - ✅
internal/handlers/settings/settings_handlers.go- GET/POST API handlers - ✅
frontend/src/types/settings.generated.ts- TypeScript interface (snake_case) - ✅
frontend/src/composables/core/useSettings.generated.ts- Helper functions - ✅
config/defaults.json- Frontend defaults (snake_case) - ✅
internal/config/defaults.json- Backend defaults (snake_case)
Important: All generated files are sorted alphabetically to minimize diff changes when adding new settings.
Frontend uses snake_case everywhere (NOT camelCase):
- ✅
settings.ai_api_key(correct) - ❌
settings.aiAPIKey(incorrect)
This convention is used consistently across:
- TypeScript interfaces (
SettingsData) - Vue components
- API communication
- Event names
Edit internal/config/settings_schema.json, add to the settings object:
"your_new_setting": {
"type": "bool", // Type: "bool", "int", or "string"
"default": false, // Default value
"category": "general", // Category (see reference below)
"encrypted": false, // Set to true for sensitive data
"frontend_key": "your_new_setting" // Use same snake_case as key
}Schema Properties:
| Property | Type | Description |
|---|---|---|
type |
string | Required: "bool", "int", or "string" |
default |
mixed | Required: Default value (must match type) |
category |
string | Required: See Categories below |
encrypted |
boolean | Required: true for sensitive data (API keys, passwords) |
frontend_key |
string | Required: Use the same snake_case as the key (for reference) |
Note: The frontend_key is currently for reference only and should match the key in snake_case. The actual frontend implementation uses snake_case property names.
go run tools/settings-generator/main.goOutput:
🔧 Generating code from schema with 66 settings...
✓ Generated config/defaults.json
✓ Generated internal/config/defaults.json
✓ Generated internal/config/config.go
✓ Generated internal/config/settings_keys.go
✓ Generated internal/handlers/settings/settings_handlers.go
✓ Generated frontend/src/types/settings.generated.ts
✓ Generated frontend/src/composables/core/useSettings.generated.ts
✨ All files generated successfully!
This automatically generates all the boilerplate code for both backend and frontend.
Find the appropriate section and add:
yourNewSetting: 'Your New Setting',
yourNewSettingDesc: 'Description of what this setting does',yourNewSetting: '您的新设置',
yourNewSettingDesc: '此设置功能的描述',Add the setting UI to the appropriate settings component.
Example - frontend/src/components/modals/settings/general/GeneralSettings.vue:
<SettingItem
:title="t('yourNewSetting')"
:description="t('yourNewSettingDesc')"
>
<Toggle
:model-value="settings.your_new_setting"
@update:model-value="updateSetting('your_new_setting', $event)"
/>
</SettingItem>UI Component Examples:
<!-- Boolean/Toggle -->
<Toggle
:model-value="settings.your_setting"
@update:model-value="updateSetting('your_setting', $event)"
/>
<!-- String/Input -->
<Input
v-model="settings.your_setting"
@change="updateSetting('your_setting', $event)"
/>
<!-- Integer/Number -->
<Input
v-model.number="settings.your_setting"
type="number"
@change="updateSetting('your_setting', $event)"
/>
<!-- Select/Enum -->
<Select
v-model="settings.your_setting"
:options="[{value: 'option1', label: 'Option 1'}, ...]"
@change="updateSetting('your_setting', $event)"
/>If the setting affects app behavior, implement the logic.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useSettings } from '@/composables/core/useSettings'
const { settings } = useSettings()
const featureEnabled = ref(false)
onMounted(() => {
// Apply the setting
featureEnabled.value = settings.value.your_new_setting
})
// Listen for changes
window.addEventListener('your-new-setting-changed', (event: any) => {
featureEnabled.value = event.detail.value
})
</script>Create frontend/src/composables/core/useYourFeature.ts:
import { computed } from 'vue'
import { useSettings } from './useSettings'
export function useYourFeature() {
const { settings } = useSettings()
const featureEnabled = computed(() => settings.value.your_new_setting)
return {
featureEnabled
}
}# Backend
go build
# Frontend
cd frontend
npm run build
# Or run full dev mode
cd ..
wails3 devLet's walk through adding a complete setting from start to finish.
We want to add a setting that automatically collapses the sidebar on startup.
Edit internal/config/settings_schema.json:
"auto_collapse_sidebar": {
"type": "bool",
"default": false,
"category": "general",
"encrypted": false,
"frontend_key": "auto_collapse_sidebar"
}Why these values?
type: "bool"- It's a toggle/checkbox settingdefault: false- Most users want sidebar expanded by defaultcategory: "general"- It's a general UI preferenceencrypted: false- Not sensitive datafrontend_key: "auto_collapse_sidebar"- Use snake_case (same as key)
go run tools/settings-generator/main.goWhat was generated?
-
internal/config/config.go- Added
AutoCollapseSidebar boolfield to Defaults struct - Added switch case for
GetString("auto_collapse_sidebar")
- Added
-
internal/config/settings_keys.go- Added
"auto_collapse_sidebar"to keys array
- Added
-
internal/handlers/settings/settings_handlers.go- Added GET:
autoCollapseSidebar, _ := h.DB.GetSetting("auto_collapse_sidebar") - Added JSON response:
"auto_collapse_sidebar": autoCollapseSidebar - Added POST field:
AutoCollapseSidebar string \json:"auto_collapse_sidebar"`` - Added save logic:
if req.AutoCollapseSidebar != "" { h.DB.SetSetting(...) }
- Added GET:
-
frontend/src/types/settings.generated.ts- Added:
auto_collapse_sidebar: boolean;
- Added:
-
frontend/src/composables/core/useSettings.generated.ts- Added:
auto_collapse_sidebar: false,to defaults - Added fetch:
auto_collapse_sidebar: data.auto_collapse_sidebar === 'true', - Added save:
auto_collapse_sidebar: (settingsRef.value.auto_collapse_sidebar ?? settingsDefaults.auto_collapse_sidebar).toString(), - Added event:
window.dispatchEvent(new CustomEvent('auto-collapse-sidebar-changed', ...))
- Added:
-
config/defaults.json&internal/config/defaults.json- Added:
"auto_collapse_sidebar": false
- Added:
English (frontend/src/i18n/locales/en.ts):
autoCollapseSidebar: 'Auto Collapse Sidebar',
autoCollapseSidebarDesc: 'Automatically collapse the sidebar when the app starts',Chinese (frontend/src/i18n/locales/zh.ts):
autoCollapseSidebar: '自动折叠侧边栏',
autoCollapseSidebarDesc: '应用启动时自动折叠侧边栏',Add to frontend/src/components/modals/settings/general/GeneralSettings.vue:
<SettingItem
:title="t('autoCollapseSidebar')"
:description="t('autoCollapseSidebarDesc')"
>
<Toggle
:model-value="settings.auto_collapse_sidebar"
@update:model-value="updateSetting('auto_collapse_sidebar', $event)"
/>
</SettingItem>Place it near related settings (like theme, startup on boot).
In your sidebar component:
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useSettings } from '@/composables/core/useSettings'
const { settings } = useSettings()
const isCollapsed = ref(false)
onMounted(() => {
// Apply the setting
isCollapsed.value = settings.value.auto_collapse_sidebar
})
// Listen for changes
window.addEventListener('auto-collapse-sidebar-changed', (event: any) => {
isCollapsed.value = event.detail.value
})
</script>
<template>
<aside :class="{ collapsed: isCollapsed }">
<!-- Sidebar content -->
</aside>
</template>
<style scoped>
aside.collapsed {
width: 60px;
}
</style>- Open Settings → General
- Find "Auto Collapse Sidebar" setting
- Toggle it on
- Close and reopen the app
- ✅ Verify sidebar is collapsed on startup
- Toggle it off
- Close and reopen the app
- ✅ Verify sidebar is expanded on startup
SELECT * FROM settings WHERE key = 'auto_collapse_sidebar';Should show:
key | value
------------------------+-------
auto_collapse_sidebar | true
GET /api/settings:
curl http://localhost:5343/api/settingsShould include:
{
"auto_collapse_sidebar": "true"
}POST /api/settings:
curl -X POST http://localhost:5343/api/settings \
-H "Content-Type: application/json" \
-d '{"auto_collapse_sidebar": "false"}'Should return 200 OK.
- Schema added to
settings_schema.json - Code generator ran successfully
- Backend compiles without errors
- Frontend compiles without errors
- English translations added
- Chinese translations added
- UI component added (Toggle in GeneralSettings)
- Feature logic implemented (sidebar collapse)
- Setting appears in settings modal
- Setting saves to database
- Setting loads on startup
- API GET returns correct value
- API POST saves value correctly
| Schema Type | Go Type | TypeScript Type | Example |
|---|---|---|---|
"bool" |
bool |
boolean |
true, false |
"int" |
int |
number |
30, 500 |
"string" |
string |
string |
"en", "openai" |
Use the appropriate category for your setting:
| Category | Description | Example Settings |
|---|---|---|
general |
General app settings | theme, language, shortcuts |
reading |
Reading/viewing preferences | view mode, hover mark as read, show hidden |
translation |
Translation settings | provider, target language, API keys |
ai |
AI-related settings | API key, model, prompts, usage limit |
summary |
Article summary settings | summary length, trigger mode |
storage |
Cache and storage settings | cache size, cleanup, max age |
network |
Network and proxy settings | proxy, bandwidth, concurrent refreshes |
integrations |
Third-party integrations | Obsidian, FreshRSS |
internal |
Internal app state (no UI) | window position, last update |
For sensitive data (API keys, passwords), set "encrypted": true:
"my_api_key": {
"type": "string",
"default": "",
"category": "integrations",
"encrypted": true, // ← Important!
"frontend_key": "my_api_key"
}Encrypted settings are automatically:
- Stored encrypted in the database
- Fetched using
GetEncryptedSetting()instead ofGetSetting() - Saved using
SetEncryptedSetting()instead ofSetSetting()
Important: Frontend uses snake_case everywhere (not camelCase).
| Backend Key (JSON) | Frontend Property (TypeScript) |
|---|---|
update_interval |
settings.update_interval ✅ |
startup_on_boot |
settings.startup_on_boot ✅ |
deepl_api_key |
settings.deepl_api_key ✅ |
ai_endpoint |
settings.ai_endpoint ✅ |
ai_chat_enabled |
settings.ai_chat_enabled ✅ |
The frontend_key in the schema is for reference and should match the key in snake_case.
Boolean Setting:
"enable_feature": {
"type": "bool",
"default": true,
"category": "general",
"encrypted": false,
"frontend_key": "enable_feature"
}Usage in Vue:
<Toggle
:model-value="settings.enable_feature"
@update:model-value="updateSetting('enable_feature', $event)"
/>Integer Setting:
"max_items": {
"type": "int",
"default": 100,
"category": "storage",
"encrypted": false,
"frontend_key": "max_items"
}String Setting:
"api_endpoint": {
"type": "string",
"default": "https://api.example.com",
"category": "integrations",
"encrypted": false,
"frontend_key": "api_endpoint"
}Encrypted Setting:
"api_secret": {
"type": "string",
"default": "",
"category": "integrations",
"encrypted": true, // ← Encrypts in DB
"frontend_key": "api_secret"
}For non-internal settings, change events are automatically dispatched. Listen to them like this:
window.addEventListener('your-setting-key-changed', (event) => {
const { value } = event.detail
console.log('Setting changed to:', value)
})Event name format: {key in kebab-case}-changed
Examples:
auto_collapse_sidebar→auto-collapse-sidebar-changedai_api_key→ai-api-key-changedai_chat_enabled→ai-chat-enabled-changed
❌ Wrong:
"type": "boolean", // Should be "bool"
"category": "General", // Should be lowercase
"frontend_key": "myAPIKey" // Should be snake_case (my_api_key)✅ Correct:
"type": "bool",
"category": "general",
"frontend_key": "my_api_key"Problem: go build fails after adding a setting
Solution:
- Check that your
settings_schema.jsonhas valid JSON syntax (no missing commas) - Verify
typeis one of:"bool","int","string" - Verify
categoryis a valid category - Run generator again:
go run tools/settings-generator/main.go
Problem: Property 'my_setting' does not exist
Solution:
- Make sure you ran the generator
- Check
frontend/src/types/settings.generated.tsexists and has your setting - Try
npm run buildin frontend directory - Restart TypeScript server in VSCode
Problem: Toggle doesn't show in settings modal
Solution:
- Check that you added the
<SettingItem>component - Verify the translation keys match
- Check browser console for errors
- Try hard refresh (Ctrl+Shift+R)
Problem: Toggle changes but resets on restart
Solution:
- Open browser DevTools → Network tab
- Check if POST to
/api/settingsis sent - Check response status (should be 200 OK)
- Check database directly via SQLite browser
- Verify the key name matches in schema
If you have existing manually-written settings code:
- ✅ Ensure all settings are defined in
internal/config/settings_schema.json - ✅ Run the generator:
go run tools/settings-generator/main.go - ✅ Review and commit the generated files
- ✅ Delete any manual setting-related code that's now replaced
The generated code is compatible with the existing database and API.
- Use descriptive names -
enable_auto_syncnoteas - Choose appropriate types - Use
boolfor toggles,intfor numbers - Set sensible defaults - What should the setting be for new users?
- Add translations - Always add both English and Chinese
- Use categories - This helps organize the settings UI
- Encrypt sensitive data - API keys, passwords, tokens
- Test after adding - Run the app and verify the setting works
- Document complex settings - Add comments if behavior is non-obvious
- Use snake_case - Frontend uses snake_case consistently (not camelCase)
- Keep frontend_key same as key - The
frontend_keyshould match the setting key
Old workflow: Edit 11 files, ~100 lines of code, high chance of errors
New workflow: Edit 1 file (5 lines), run 1 command, done!
This optimization:
- ✅ Reduces development time by ~90%
- ✅ Eliminates copy-paste errors
- ✅ Ensures consistency between frontend and backend
- ✅ Maintains type safety automatically
- ✅ Makes adding new settings trivial
- ✅ Uses snake_case throughout (simpler than camelCase)
Happy coding! 🚀