Configuration System — Architecture Overview
Purpose
HR Studio 2 uses a four-level hierarchical configuration system. Every configuration variable is defined once in a central catalog (cloud.config_catalog) and overridden selectively at each level. The resolved value for any user is computed by layering the four levels in order:
Global → Tenant → Group → User
Each level can only override keys that permit it, as controlled by a per-key bitmask (cck_allowed_levels).
The four levels
1. Global
Who sets it: SuperAdmin only.
Table: cloud.config_values_global
Purpose: System-wide policy. Examples: whether self-registration is open, maintenance mode, default password policy.
Override rule: A global value is applied after the catalog default. Tenant and users cannot override global-only keys.
2. Tenant
Who sets it: TenantAdmin.
Table: cloud.config_values_tenant
Purpose: Per-tenant customization. Examples: app name, active modules, primary color, feature flags.
Override rule: Tenant values override global values where permitted. Tenants cannot loosen security constraints set at the global level (enforced in the write endpoint).
3. Group
Who sets it: TenantAdmin (assigns overrides to HR grouping dimensions).
Table: cloud.config_values_group
Purpose: Apply different configuration to subsets of employees. Examples: compact UI for field workers, hidden salary sections for team leaders.
Group types: mansione, qualifica, sede_op, tipo_tempo, gruppo_tipo, static_list, dynamic_list.
Conflict resolution: When a user matches multiple groups that disagree on a key, the system applies the system.group_conflict_strategy setting (permissive or restrictive). See Conflict Resolution below.
4. User
Who sets it: The user themselves (for UI preferences) or a TenantAdmin.
Table: cloud.config_values_user
Purpose: Individual preferences. Examples: dark/light theme, sidebar state, dashboard layout.
Override rule: User values are applied last and win over all other levels for permitted keys.
Configuration catalog
cloud.config_catalog is the single source of truth for all known config keys.
Each key has:
cck_code— dotted name used everywhere (e.g.ui.theme,auth.password.min_length)cck_value_type—bool,int,string, orjsoncck_default_value— the baseline value if no level overrides itcck_allowed_levels— bitmask (1=global, 2=tenant, 4=group, 8=user)cck_is_sensitive— iftrue, the value is resolved server-side only and never sent to the frontend
New keys can be added to the catalog at any time without a deployment — new catalog rows are picked up automatically at the next login.
Group membership resolution
Group memberships are determined at login time from the HR data. They are never cached on the user record. This ensures changes (e.g. a department transfer or list membership change) take effect at the next login automatically.
The backend uses an IUserGroupMembershipProvider interface:
- Default (cloud module only): returns empty — group overrides have no effect.
- HR module: queries the HR schema and returns the user's
mansione,qualifica,sede_op,tipo_tempo,gruppo_tipomemberships, plus anystatic_listmemberships (hr.static_lists+hr.static_list_members) anddynamic_listmemberships (evaluated by running each list's criteria fromhr.dynamic_list_detailsagainst the employee record).
Static lists (static_list)
Tenant admins create named lists of specific employees (hr.static_lists). Membership is explicit — an employee is added or removed from a list manually.
Used as cdv_group_type = 'static_list' in cloud.config_values_group.
Dynamic lists (dynamic_list)
Tenant admins define filter criteria (hr.dynamic_lists + hr.dynamic_list_details). Membership is computed: an employee belongs to the list if they satisfy the criteria at login time.
Example — a dynamic list "Milan Developers":
- Criterion 1:
mansione_code = 'DEV' - Criterion 2:
sede_op_code = 'MI' - Logic:
AND
Used as cdv_group_type = 'dynamic_list' in cloud.config_values_group.
Conflict resolution
When a user belongs to multiple groups that provide different values for the same key, the system reads system.group_conflict_strategy (itself a catalog key, allowed at global + tenant level):
| Strategy | bool | int | string | json |
|---|---|---|---|---|
restrictive (default) | AND — true only if all groups agree | min value | alphabetically first | intersection |
permissive | OR — true if any group says true | max value | alphabetically last | union |
For permissions.section_visibility (JSON object):
- Restrictive: a section is visible only if all matching groups allow it.
- Permissive: a section is visible if any matching group allows it.
Resolution algorithm
ConfigMergeService.ResolveForUserAsync produces the full resolved config map for a user:
1. Load all active catalog keys → start with cck_default_value for each
2. Apply cloud.config_values_global (bit 0 = global)
3. Apply cloud.config_values_tenant (bit 1 = tenant), for this user's tenant
4. Resolve group memberships via IUserGroupMembershipProvider
→ Apply cloud.config_values_group matching (group_type, group_code) pairs
→ If multiple matches for same key, apply conflict strategy
5. Apply cloud.config_values_user (bit 3 = user), for this user
6. Remove all keys where cck_is_sensitive = true
7. Return resolved map: Dictionary<string, string>
The resolved map is included in the LoginResponse.Config field so the frontend does not need a separate API call.
Tenant license configuration
Feature availability is controlled exclusively by ten_license_config, not by the config catalog. There are no features.* catalog keys — which modules a tenant can access is a license concern, not a configuration concern. Use useLicenseModule("payroll") on the frontend and read ten_license_config on the backend.
cloud.tenants.ten_license_config is a JSONB column set by SuperAdmins only:
{
"type": "license",
"modules": ["hr", "payroll", "recruitment"],
"app_modules": ["mobile_checkin", "expense_report"],
"max_users": 50,
"expires_at": "2027-01-01T00:00:00Z"
}
type values: free | demo | license
The license config is included in LoginResponse.LicenseConfig and read by the useLicense() frontend hook for feature gating. The license system will be detailed in a dedicated specification.
Frontend integration
After login, the resolved config map is available in the Redux config slice:
const minLen = useConfig<number>("auth.password.min_length", 8);
const aiOn = useFeature("features.ai_assistant");
const visible = useConfig<Record<string, string[]>>("permissions.section_visibility", {});
For in-session refresh (e.g. after a TenantAdmin changes settings):
POST /api/config/user/resolve
Database tables
Cloud schema (config infrastructure)
| Table | Purpose |
|---|---|
cloud.config_catalog | Key definitions, types, defaults, level permissions |
cloud.config_values_global | System-wide overrides |
cloud.config_values_tenant | Per-tenant overrides |
cloud.config_values_group | Per-HR-group overrides |
cloud.config_values_user | Per-user overrides |
HR schema (group membership sources)
| Table | Purpose |
|---|---|
hr.static_lists | Named lists of employees (manual assignment) |
hr.static_list_members | Membership join table |
hr.dynamic_lists | Filter-based group definitions |
hr.dynamic_list_details | Individual filter criteria for dynamic lists |
API endpoints
All config management endpoints are under POST /api/config/. See Config API.