Application Settings
The Commons Config file (commons.jsonp) is read-only at runtime — once the application starts, its values cannot be changed without a restart. ApplicationSettings solves this by providing a DB-persisted model that initializes from JSONP configuration but can be modified at runtime through the Hub GUI.
This allows administrators to change operational parameters (mail engine, access defaults, feature flags) without redeploying the application, while developers maintain a reliable JSONP baseline for fresh environments.
ApplicationSettings is part of io-oidis-hub (backend) and io-oidis-hub-gui (frontend). It is available in every project that depends on the Hub stack.
How It Works
The system operates as a bridge between static JSONP configuration and runtime state:
Startup
commons.jsonp ──────► SyncAppSettings() ──────► AppSettings (DB)
(static) │ │
│ preferDBSettings? │
│ true → DB wins │ SyncTo()
│ false → JSONP wins ▼
│ Runtime config
│ (IProject)
│ │
│ │ side effects
│ ▼
│ Services start/stop
│ (e.g. mail engine)
│
Runtime
AppSettingsController ──► SaveAppSettings()
(Hub GUI) │
│ SyncWith() + Save()
▼
AppSettings (DB)
│
│ SyncTo()
▼
Runtime config updated
Key flow:
- On first startup, JSONP values seed the DB via
SyncFrom() - On subsequent startups, behavior depends on the
preferDBSettingsflag - GUI edits persist to DB and apply immediately via
SyncTo() SyncTo()can have side effects — e.g. togglingSendmail.Enabledstarts or stops the mail engine
The AppSettings Entity
The backend entity is a standard Oidis model decorated with @Entity:
import { InclusionConverter } from "@io-oidis-commons/Io/Oidis/Commons/DAO/Converters/InclusionConverter.js";
import { BaseModel } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/BaseModel.js";
import { Entity, Property } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/Decorators.js";
@Entity("appSettings")
export class AppSettings extends BaseModel<AppSettings> {
@Property({type: String, required: false})
public declare Version : string;
@Property({type: Sendmail, converter: new InclusionConverter(Sendmail)})
public declare Sendmail : Sendmail;
@Property({type: [String], default: []})
public declare DefaultAccessibleMethods : string[];
@Property({type: [String], default: []})
public declare DefaultGroupAccess : string[];
public async SyncFrom($config : IProject) : Promise<void> {
this.Version = $config.version;
await this.Sendmail.SyncFrom($config.sendmail);
await this.Save();
}
public async SyncTo($config : IProject) : Promise<void> {
await this.Sendmail.SyncTo($config.sendmail);
}
}
Properties
| Property | Type | Purpose |
|---|---|---|
Version | string | Tracks the config version — used to detect when JSONP config has changed between deployments |
Sendmail | Sendmail (nested) | Mail engine settings: enabled state, admin email list, blacklist |
DefaultAccessibleMethods | string[] | Default RPC methods accessible to all authorized users |
DefaultGroupAccess | string[] | Default group-level access permissions |
McpEnabled | boolean | Runtime toggle for the MCP endpoint. Seeded from mcp.enabled in commons.jsonp via SyncFrom (off by default), then admin-togglable — the standard config-default / DB-state feature-flag pattern. Other boolean flags (AutoActivateRegistered, AutoRestartEnabled) work the same way. |
Nested Sendmail Entity
The Sendmail property uses InclusionConverter, which embeds the child entity directly inside the parent MongoDB document rather than storing it as a separate collection. This is the standard pattern for settings that logically belong together.
@Entity("sendmail")
export class Sendmail extends BaseModel<Sendmail> {
@Property({type: Boolean, default: false})
public declare Enabled : boolean;
@Property({type: [String], default: []})
public declare AdminsList : string[];
@Property({type: [String], default: []})
public declare BlackList : string[];
public async SyncFrom($config : ISendmailConfiguration) : Promise<void> {
this.Enabled = $config.enabled;
this.AdminsList = $config.admins;
this.BlackList = $config.blackList;
}
public async SyncTo($config : ISendmailConfiguration) : Promise<void> {
const enableStateChanged : boolean = $config.enabled !== this.Enabled;
$config.enabled = this.Enabled;
$config.admins = this.AdminsList;
$config.blackList = this.BlackList;
if (enableStateChanged) {
if ($config.enabled) {
Loader.getInstance().getMailEngine().StartLoop();
} else {
Loader.getInstance().getMailEngine().StopLoop();
}
}
}
}
Notice the side effect in SyncTo — when Enabled changes, the mail engine is started or stopped immediately.
SyncFrom and SyncTo
Every entity that participates in the settings sync implements two methods:
SyncFrom($config)— reads values from the JSONP config object into the DB model, then savesSyncTo($config)— applies DB values to the runtime config object (can trigger side effects)
These methods are called by EnvironmentHelper.SyncAppSettings() during startup and by SaveAppSettings() during runtime edits.
Backend API — EnvironmentHelper
EnvironmentHelper in io-oidis-hub exposes three @Extern methods for managing ApplicationSettings:
getAppSettings()
Reads the singleton AppSettings document from the database. If no document exists yet (first startup), it creates one automatically.
@Extern()
public async getAppSettings() : Promise<AppSettings> {
const modelClass : any = this.getAppSettingsModel();
const settings : AppSettings[] = await modelClass.Find({});
let instance : AppSettings;
if (ObjectValidator.IsEmptyOrNull(settings)) {
instance = new modelClass();
await instance.Save();
} else {
if (settings.length > 1) {
LogIt.Warning("Multiple internal setting structures found in DB, using first one.");
}
instance = settings[0];
}
return instance;
}
The getAppSettingsModel() call (instead of using AppSettings directly) is the extension point — end applications override this protected method to return their extended entity class. See Extending ApplicationSettings.
SaveAppSettings($settings)
Saves settings to the database and applies them to the runtime configuration:
@Extern()
public async SaveAppSettings($settings : AppSettings) : Promise<void> {
const settings : AppSettings = await this.getAppSettings();
settings.SyncWith($settings);
await settings.Save();
await settings.SyncTo(Loader.getInstance().getAppConfiguration());
}
The flow is: SyncWith() (merge properties from the incoming model) → Save() (persist to DB) → SyncTo() (apply to runtime config with side effects).
SaveAppSettings triggers SyncTo(), which can start or stop backend services. For example, changing Sendmail.Enabled will immediately start or stop the mail engine loop. Be aware of these side effects when calling this method programmatically.
SyncAppSettings($resetDB)
Called automatically during application startup by Loader. Decides how to synchronize JSONP config with the DB based on version comparison and the preferDBSettings flag:
@Extern()
public async SyncAppSettings($resetDB : boolean = false) : Promise<void> {
const settings : AppSettings = await this.getAppSettings();
const config : IProject = Loader.getInstance().getAppConfiguration();
if ($resetDB ||
settings.Version !== config.version ||
settings.LastModified.getTime() < config.build.timestamp) {
if (config.target.preferDBSettings && !$resetDB) {
settings.Version = config.version;
await settings.Save();
await settings.SyncTo(config);
} else {
await settings.SyncFrom(config);
}
} else {
await settings.SyncTo(config);
}
}
The decision logic:
| Condition | Behavior | Source of truth |
|---|---|---|
$resetDB: true | JSONP config overwrites DB | JSONP (commons.jsonp) |
Version or timestamp changed, preferDBSettings: true | DB values kept, applied to runtime | Database |
Version or timestamp changed, preferDBSettings: false | JSONP config overwrites DB | JSONP (commons.jsonp) |
| No change detected | DB values applied to runtime (no write) | Database |
The preferDBSettings Flag
The preferDBSettings flag on target in your project configuration controls which source of truth wins when a new version is deployed:
// commons.jsonp
Io.Oidis.Hub.DAO.Resources.Data({
target: {
preferDBSettings: true
// ...
}
});
Two strategies:
true— DB wins: After the initial seeding, administrators manage settings through the GUI. JSONP changes on redeployment do not overwrite DB values. Use this for production environments where admins customize settings after deployment.false(default) — JSONP wins: Every startup resets the DB from JSONP. Use this for development or environments where config-as-code is preferred.
For development, keep preferDBSettings: false so your commons.jsonp changes always take effect on restart. For production, switch to true so admin changes made through the GUI persist across restarts and redeployments.
The Reset button in the AppSettings GUI panel always forces a full reset from JSONP (calls SyncAppSettings(true)), regardless of the preferDBSettings flag. This gives administrators an escape hatch to restore defaults.
Frontend — AppSettingsController
The Hub GUI provides AppSettingsPanel and AppSettingsController for managing settings through the browser.
AppSettingsPanel
The panel extends BasePanel and defines the UI controls:
export class AppSettingsPanel extends BasePanel {
public readonly mailEngine : CheckBox;
public readonly adminsList : TextField;
public readonly blackList : TextField;
public readonly accessMethods : TextField;
public readonly groupAccess : TextField;
public readonly saveSettings : Button;
public readonly resetSettings : Button;
constructor() {
super();
this.mailEngine = new CheckBox();
this.adminsList = new TextField();
this.blackList = new TextField();
this.accessMethods = new TextField();
this.groupAccess = new TextField();
this.saveSettings = new Button();
this.resetSettings = new Button();
}
}
The innerHtml() method renders a form with a "Mail Engine" toggle, text fields for admin list / blacklist / access methods / group access, and two buttons: Save settings and Reload from server config.
AppSettingsController
The controller extends BasePanelController with a generic type parameter for panel extensibility:
@Resolver("/appSettings")
export class AppSettingsController<Panel extends AppSettingsPanel> extends BasePanelController<Panel> {
protected readonly dataProcessor : EnvironmentConnector;
constructor($instance : Panel) {
super($instance);
this.dataProcessor = EnvironmentConnector.getInstanceSingleton();
}
}
Lifecycle methods:
FetchData()— loads settings from the backend viaEnvironmentConnector.getAppSettings()and populates the panel controls. Array values are joined with commas for display in text fields.resolver()— binds the Save and Reset buttons:
protected async resolver($instance? : AppSettingsPanel) : Promise<void> {
EventsBinding.SingleClick($instance.saveSettings, async () : Promise<void> => {
try {
await this.dataProcessor.SaveAppSettings(
this.processSettings($instance, await this.dataProcessor.getAppSettings())
);
await this.FetchData();
} catch (ex) {
LogIt.Error(ex);
}
});
EventsBinding.SingleClick($instance.resetSettings, async () : Promise<void> => {
try {
await this.dataProcessor.SyncAppSettings(true);
await this.FetchData();
} catch (ex) {
LogIt.Error(ex);
}
});
}
processSettings()— reads control values from the panel and maps them into the model. Comma-separated text fields are split into arrays usingStringUtils.Split().
EnvironmentConnector
The frontend communicates with EnvironmentHelper through EnvironmentConnector, which wraps each backend method as an asyncInvoke call:
public getAppSettings() : Promise<AppSettings> {
return this.asyncInvoke("getAppSettings");
}
public SaveAppSettings($settings : AppSettings) : Promise<void> {
return this.asyncInvoke("SaveAppSettings", $settings);
}
public async SyncAppSettings($resetDB : boolean = false) : Promise<void> {
return await this.asyncInvoke("SyncAppSettings", $resetDB);
}
Extending ApplicationSettings
End applications commonly extend the base ApplicationSettings with custom properties. The system is designed for this through inheritance at every layer.
Step 1 — Extend the Backend Entity
Create your own AppSettings that extends the Hub version:
import { Entity, Property } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/Decorators.js";
import { AppSettings as HubAppSettings } from "@io-oidis-hub/Io/Oidis/Hub/DAO/Models/AppSettings.js";
@Entity("appSettings")
export class AppSettings extends HubAppSettings {
@Property({type: String, default: ""})
public declare ApiEndpoint : string;
@Property({type: Boolean, default: false})
public declare FeatureXEnabled : boolean;
}
The extended entity must use the same @Entity("appSettings") collection name. The framework reads a singleton document from this collection — using a different name would create a separate, disconnected settings document.
Step 2 — Override SyncFrom / SyncTo
If your custom properties should be pre-populated from JSONP or applied to the runtime config, override the sync methods:
public async SyncFrom($config : IProject) : Promise<void> {
await super.SyncFrom($config);
this.ApiEndpoint = $config.custom?.apiEndpoint ?? "";
this.FeatureXEnabled = $config.custom?.featureXEnabled ?? false;
await this.Save();
}
public async SyncTo($config : IProject) : Promise<void> {
await super.SyncTo($config);
// Apply custom settings to runtime config or trigger side effects
}
Step 3 — Override getAppSettingsModel
In your extended EnvironmentHelper, override the protected factory method so the framework uses your entity class:
import { EnvironmentHelper as HubEnvironmentHelper } from "@io-oidis-hub/Io/Oidis/Hub/Utils/EnvironmentHelper.js";
import { AppSettings } from "../DAO/Models/AppSettings.js";
export class EnvironmentHelper extends HubEnvironmentHelper {
protected getAppSettingsModel() : any {
return AppSettings;
}
}
This ensures getAppSettings(), SaveAppSettings(), and SyncAppSettings() all operate on your extended entity.
Step 4 — Extend the Frontend Panel
Add custom controls to the panel by extending AppSettingsPanel:
import { AppSettingsPanel as HubPanel } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Panels/Dashboard/AppSettingsPanel.js";
import { CheckBox } from "@io-oidis-usercontrols/Io/Oidis/UserControls/Bootstrap/UserControls/CheckBox.js";
import { TextField } from "@io-oidis-usercontrols/Io/Oidis/UserControls/Bootstrap/UserControls/TextField.js";
export class AppSettingsPanel extends HubPanel {
public readonly apiEndpoint : TextField;
public readonly featureXToggle : CheckBox;
constructor() {
super();
this.apiEndpoint = new TextField();
this.featureXToggle = new CheckBox();
}
protected getSettings() : string {
return `
${super.getSettings()}
<div class="input-group" style="margin-bottom: 5px;">
<span class="input-group-text" style="width: 200px;">API Endpoint</span>
<input class="form-control" type="text" data-oidis-bind="${this.apiEndpoint}" />
</div>`;
}
protected getFeatures() : string {
return `
${super.getFeatures()}
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" data-oidis-bind="${this.featureXToggle}" />
<label class="form-check-label">Feature X</label>
</div>`;
}
}
Step 5 — Extend the Controller
Override FetchData and processSettings to handle custom fields:
import { AppSettingsController as HubController } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Controllers/Panels/AppSettingsController.js";
import { AppSettings } from "../Models/AppSettings.js";
import { AppSettingsPanel } from "../Panels/Dashboard/AppSettingsPanel.js";
@Resolver("/appSettings")
export class AppSettingsController extends HubController<AppSettingsPanel> {
public async FetchData() : Promise<void> {
await super.FetchData();
const instance : AppSettingsPanel = this.getInstanceOwner();
const settings : AppSettings = await this.dataProcessor.getAppSettings() as AppSettings;
instance.apiEndpoint.Value(settings.ApiEndpoint);
instance.featureXToggle.Value(settings.FeatureXEnabled);
}
protected processSettings($instance : AppSettingsPanel, $model : AppSettings) : AppSettings {
super.processSettings($instance, $model);
$model.ApiEndpoint = $instance.apiEndpoint.Value();
$model.FeatureXEnabled = $instance.featureXToggle.Value();
return $model;
}
}
The base AppSettingsController uses the generic type parameter <Panel extends AppSettingsPanel>, specifically designed for this extension pattern. Your extended controller gets full type safety for the custom panel controls.
Step 6 — Mount in the Dashboard, not the App MainPage
AppSettings is administration, not regular app navigation. Mount the extended panel
in the Hub Dashboard (/dashboard/appSettings) by overriding the dashboard tab —
do not add it as a tab to the application's MainPage. Putting admin-only
configuration alongside end-user tabs (Home / Reports / etc.) muddles the navigation
model and forces an AccessValidator() gate on what should naturally live behind the
dashboard's existing admin guard.
Extend Hub's DashboardPage and replace the APP_SETTINGS tab with your own:
import { JsonUtils } from "@io-oidis-commons/Io/Oidis/Commons/Utils/JsonUtils.js";
import {
DashboardPage as Parent, DashboardTabIds
} from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Pages/DashboardPage.js";
import { ITabDescriptor } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Primitives/BasePageLayout.js";
import { AppSettingsController } from "../Controllers/Panels/Dashboard/AppSettingsController.js";
import { AppSettingsPanel } from "../Panels/Dashboard/AppSettingsPanel.js";
export class DashboardPage extends Parent {
protected getTabsConfiguration() : ITabDescriptor[] {
return JsonUtils.Extend(super.getTabsConfiguration(), {
// Disable Hub tabs you don't ship
[DashboardTabIds.AGENTS] : {enabled: false},
[DashboardTabIds.REPORTS] : {enabled: false},
// Swap the AppSettings tab for the extended one
[DashboardTabIds.APP_SETTINGS]: {
controller: AppSettingsController,
content : AppSettingsPanel
}
});
}
}
Then mount the page at /dashboard with a dedicated controller:
@Resolver("/dashboard")
export class DashboardPageController extends Parent {
constructor() {
super();
this.setInstanceOwner(new DashboardPage());
this.setPageTitle("My App - Admin");
}
}
Reference implementations live in cz-na2000-milena-gui and cz-fiksek-stela-gui —
both ship a Gui/Pages/DashboardPage.ts + Gui/Controllers/Pages/DashboardPageController.ts
pair that follows exactly this pattern.
ApplicationSettings vs User Profile
ApplicationSettings manages app-wide configuration. Oidis also supports per-user settings through the User Profile system, which uses browser-level persistence:
| Aspect | ApplicationSettings | User Profile |
|---|---|---|
| Scope | App-wide (all users) | Per-user |
| Storage | MongoDB (appSettings collection) | Browser (localStorage) |
| Access | Admin-only GUI panel | Each user manages their own |
| Use case | Mail engine, default permissions, feature flags | UI preferences, dashboard layout, language |
| JSONP sync | Yes — initialized from commons.jsonp | No server-side sync |
| Extensible | Yes — entity + panel + controller | Yes — via PersistenceHandler |
For per-user preferences and personal data, see User Profile. For browser-level UI state persistence (form values, autofill, viewer state), see PersistenceHandler.
Quick Reference
| What | Location | Package |
|---|---|---|
| AppSettings entity (BE) | Hub/DAO/Models/AppSettings.ts | io-oidis-hub |
| Sendmail entity (BE) | Hub/DAO/Models/Sendmail.ts | io-oidis-hub |
| EnvironmentHelper (BE API) | Hub/Utils/EnvironmentHelper.ts | io-oidis-hub |
| AppSettings model (FE) | Hub/Gui/Models/AppSettings.ts | io-oidis-hub-gui |
| AppSettingsPanel (FE) | Hub/Gui/Panels/Dashboard/AppSettingsPanel.ts | io-oidis-hub-gui |
| AppSettingsController (FE) | Hub/Gui/Controllers/Panels/AppSettingsController.ts | io-oidis-hub-gui |
| EnvironmentConnector (FE) | Hub/Gui/Connectors/EnvironmentConnector.ts | io-oidis-hub-gui |
preferDBSettings flag | target.preferDBSettings in commons.jsonp | Your project |
See also: Commons Config | Frontend Configuration | Entity and Property | Panels and Controllers | Events System