# System Configuration Development
System configuration is defined with the DynamicConfig object and stores configuration items that can be changed at runtime, such as the attachment storage method or the WebSocket heartbeat interval. Both plugins and dynamic logic can read these configurations.
# Target Audience
This document is intended for developers, implementers and operations staff of the system, as well as advanced users who need to configure the system.
# Configuration Definition
Configurations are maintained under the menu System Config > Configs and are organized in a tree structure. The related fields are as follows:
| Field | Description |
|---|---|
| name | Name of the configuration; unique, cannot be changed after creation |
| key | The key used to get the configuration in frontend and backend APIs; unique, cannot be changed after creation |
| value | The value of the configuration, stored as text |
| parent | The parent configuration, used to organize related configurations into a tree |
| description | Description of the configuration |
| modifyRemark | Modification remark |
| displaySequence | Display order in the configuration tree. When creating a configuration without a parent and leaving it empty, the platform automatically uses the current maximum across all top-level configurations plus 10; when creating a configuration with a parent it must be filled in, see the warning below |
| allowPublicAccess | Whether users who are not logged in may read this configuration through the frontend API; defaults to false |
| icon | The icon shown in the configuration tree |
| isSystem | Whether this is a built-in system configuration. Outside the development environment, built-in system configurations cannot be deleted |
The header of the CSV seed data is as follows:
name,key(*),value,parent.key,description,modifyRemark,displaySequence,allowPublicAccess,icon,isSystem
# Revision History
When value, allowPublicAccess, description or modifyRemark is modified, the platform saves a revision history. In "History version" of the edit form, you can compare the differences between versions or set a historical revision as the latest version.
# Creating and Deleting
Creating child configurations and deleting configurations fail in the current version
The display order on creation and the check before deletion are implemented by two built-in object hooks (DynamicConfigBeforeCreate, DynamicConfigBeforeDelete). Both are defective in the current version, and both fail with groovy.lang.MissingPropertyException: No such property: children for class: tech.muyan.dynamic.config.DynamicConfig:
- Creating a configuration with a parent: saving fails when displaySequence is not filled in. The create form has a "Parent" field but no display order field, so child configurations cannot be created in the UI. Create them through CSV seed data or the data API (
POST /data/DynamicConfig; all API paths on this page omit the/apiprefix, see Address Prefix) and fill indisplaySequenceexplicitly. Creating a configuration without a parent is not affected. - Deleting a configuration: deleting any configuration fails, whether or not it has child configurations. If you really need to delete one, find
DynamicConfigBeforeDeleteunderDevelopment > Object Hooks, temporarily turn off "Active" in its edit form, delete the configuration, and then turn it back on immediately (in testing, turning it off takes effect immediately). While it is turned off, the check that "a configuration with child configurations cannot be deleted" is not performed either, so make sure yourself that the configuration has no child configurations before deleting it.
By design, a configuration with child configurations cannot be deleted until all its child configurations are deleted, and outside the development environment, built-in system (isSystem) configurations cannot be deleted. Deleting a configuration is a physical delete; deleted configurations do not appear under the Trash > Dynamic Config Trash menu.
# Environment Variables Take Precedence
When reading a configuration, the platform first looks for an operating system environment variable with the corresponding name. If it exists, its value is used directly and the configuration in the database is not read; only when the environment variable does not exist is the value in DynamicConfig used.
The environment variable name is obtained by replacing every . in the configuration key with _, without changing case. For example, the configuration key attachment.storageEngine corresponds to the environment variable attachment_storageEngine.
TIP
- Configuration values are cached in memory after being read. After a configuration is modified in the UI, the cache is updated immediately to the new value in the database, even if an environment variable is set for that key; the environment variable only takes precedence again after the backend restarts.
- The frontend API
GET /config/{key}follows the same environment-variable-first rule, provided that a configuration record for that key exists in the database.
# Frontend Configuration API
The frontend gets configurations through getConfig provided by @muyantech/frontend-lib. It calls the API GET /config/{key}, which returns { key, value }:
import { getConfig } from "@muyantech/frontend-lib";
// Calls GET /api/config/{key} and resolves to { key, value }; value is a string
const config: any = await getConfig('websocket.heartbeatInterval');
const heartbeatInterval = Number(config?.value ?? 20); 2
3
4
API rules:
- If the key does not exist in the database, an empty object
{}is returned. - Users who are not logged in can only read configurations whose
allowPublicAccessistrue; otherwise 403 is returned. - Logged-in users can read any configuration. Do not rely on this switch alone to protect sensitive information (keys, passwords); you can provide it through environment variables instead.
There is also GET /config/system, which requires no login and returns the system information the frontend needs at startup (for example, whether presigned upload is enabled for attachments).
# Backend Configuration API
The backend configuration service interface is tech.muyan.api.config.DynamicConfigService, which provides the following methods:
// Interface tech.muyan.api.config.DynamicConfigService
// Gets the configuration value (a string) by key; returns null if it does not exist
String getByKey(String key);
// Gets the configuration value by key and converts it to the given type; returns null if it does not exist
<T> T getByKey(String key, Class<T> typeClass);
// Gets the configuration value by key and converts it to the type of the default value; returns defaultValue if it does not exist
<T> T getByKeyOrDefault(String key, T defaultValue);
// Listens for changes of a key; the callback receives (old value, new value), and the return value is used to unregister the listener
ListenerUnregister register(String key, DynamicConfigChangeEventListener listener);
2
3
4
5
6
7
8
9
10
11
12
13
It is used as follows:
In a plugin's Service or other Spring bean, inject
tech.muyan.api.config.DynamicConfigServicedirectly.In dynamic logic, get the service through the static field
tech.muyan.ConfigHelper.dynamicConfigService:
import tech.muyan.ConfigHelper
Integer interval = ConfigHelper.dynamicConfigService
.getByKeyOrDefault("websocket.heartbeatInterval", 20)
2
3
4
- On fields of a plugin component (a class implementing
tech.muyan.api.MuyanPlatformComponent), you can use the annotation@tech.muyan.api.annotations.DynamicConfig("config key"). When the plugin is loaded, the platform writes the configuration value through the field's public setter, and calls the setter again automatically after the configuration changes:
import tech.muyan.api.annotations.DynamicConfig
class HeartbeatSettings implements tech.muyan.api.MuyanPlatformComponent {
@DynamicConfig("websocket.heartbeatInterval")
Integer heartbeatInterval
}
2
3
4
5
6