Skip to main content

Configuration System

HR Studio 2 uses a four-level hierarchical configuration system. Every configuration variable is defined once in a central catalog and overridden selectively at each level.

For the full architecture reference, see config-system-architecture.


How it works — the four levels

Global → Tenant → Group → User
LevelSet byScope
Global (bit 1)SuperAdminAll tenants
Tenant (bit 2)TenantAdminAll users of one tenant
Group (bit 4)TenantAdminEmployees in an HR group
User (bit 8)User / TenantAdminOne individual

A key's cck_allowed_levels bitmask controls which levels may override it. For example, cck_allowed_levels = 3 (binary 0011) means only Global and Tenant may override it.


Adding a new config key

Adding a new config variable requires changes in four places:

1. Add a row to cloud.config_catalog

Create a new EF Core migration or insert the row in an existing data seed:

INSERT INTO cloud.config_catalog
(cck_code, cck_category, cck_label, cck_value_type, cck_default_value, cck_allowed_levels)
VALUES
('features.module.training', 'features', 'Training module enabled', 'bool', 'false', 3);
-- allowed_levels = 3 → Global (1) + Tenant (2)

Or via the API (SuperAdmin only):

POST /api/config/catalog/create
{
"code": "features.module.training",
"category": "features",
"label": "Training module enabled",
"valueType": "bool",
"defaultValue": "false",
"allowedLevels": 3
}

Key design questions to answer before adding a new key:

QuestionWhy it matters
What is the right default value?Used when no level overrides the key
Which levels should be able to override it?Set the bitmask accordingly
Is this a security constraint?If yes, add a global-only or constrained write rule in ConfigController.TenantSet
Does it need to stay server-side?Set isSensitive = true — it will never be sent to the frontend
What type is the value?bool, int, string, or json — determines conflict resolution and coercion
Is this about feature/module availability?Do not use the config catalog. Feature availability is controlled by ten_license_config on cloud.tenants. Use useLicenseModule() on the frontend.

2. Use it in the backend (if sensitive or server-side logic)

// Inject ConfigMergeService
var configMap = await configMerge.ResolveForUserAsync(userId, tenantId, ct);
var isEnabled = configMap.TryGetValue("features.module.training", out var v) && v == "true";

3. Use it in the frontend

After login, the full config map is in the Redux store. Use the hooks:

// Boolean feature flag
const hasTraining = useFeature("features.module.training");

// Typed value with fallback
const minLen = useConfig<number>("auth.password.min_length", 8);

// JSON value
const visibility = useConfig<Record<string, string[]>>("permissions.section_visibility", {});

4. Document it

Add the key to the catalog table in docs/config-system-design.md (repo root) and update the relevant API or architecture docs.


Frontend hooks

Status: pending reimplementation.
The config hooks listed below were designed for the original frontend and are pending reimplementation in the current Zono-based frontend. The config map is already delivered at login and stored in the Redux store (state.auth.config); direct store reads work in the meantime.

Planned hooks — to be implemented in frontend/src/hooks/:

// Read a typed value — T is inferred from the fallback
useConfig<T>(key: string, fallback: T): T

// Boolean shorthand for features.*
useFeature(key: string): boolean

// Full config map for admin UI
useAppConfig(): Record<string, string>

// License config
useLicense(): LicenseConfig | null

// Returns true if a specific module is licensed
useLicenseModule(module: string): boolean

// Re-fetch config without re-login (call after TenantAdmin changes settings)
useConfigRefresh(): () => Promise<void>

Until the hooks are reimplemented, read config values directly from the store:

const config = useAppSelector((s) => s.auth.config);
const timeoutMinutes = parseInt(config["ui.lock.timeout_minutes"] ?? "15", 10);

Example: feature-gating a route (planned)

import { useFeature } from "../hooks/useConfig";
import { Navigate } from "react-router-dom";

const PayrollPage = () => {
const hasPayroll = useFeature("features.module.payroll");
if (!hasPayroll) return <Navigate to="/dashboard" />;
return <PayrollDashboard />;
};

Example: reading a JSON config value (planned)

import { useConfig } from "../hooks/useConfig";

const SalarySection = ({ employeeId }: { employeeId: string }) => {
const visibility = useConfig<Record<string, string[]>>(
"permissions.section_visibility",
{}
);
const allowedRoles = visibility["employee.salary"] ?? [];
// ...
};

Group-level overrides

Group overrides apply to employees based on their HR attributes. The group types are:

group_typeSourceExample codes
mansioneJob functionDEV, MGR
qualificaQualificationJR, SR, LEAD
sede_opOperational siteMI, RM, REMOTE
tipo_tempoWorking time typeFT, PT_50
gruppo_tipoCustom groupingtenant-defined
static_listNamed employee listtenant-defined
dynamic_listFilter-based listtenant-defined

Group memberships are resolved fresh at every login by IUserGroupMembershipProvider. When the HR module is active, this queries the HR schema. Until then, the default implementation returns no memberships.

Conflict resolution

When a user belongs to multiple groups that set different values for the same key, system.group_conflict_strategy decides:

Strategyboolintstringjson
restrictive (default)ANDminfirst alphabeticallyintersection
permissiveORmaxlast alphabeticallyunion

Sensitive keys

If cck_is_sensitive = true, the key is computed and used server-side only. It is stripped from the config map before the response is sent to the client.

Use this for keys that should never be exposed to the browser — for example, internal API keys, IP allow-lists, or secret feature flags.


In-session config refresh

If a TenantAdmin changes a configuration value and you want the current user's config to update without forcing a re-login:

const refreshConfig = useConfigRefresh();
await refreshConfig(); // calls POST /api/config/user/resolve and updates Redux