Skip to main content

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_typebool, int, string, or json
  • cck_default_value — the baseline value if no level overrides it
  • cck_allowed_levels — bitmask (1=global, 2=tenant, 4=group, 8=user)
  • cck_is_sensitive — if true, 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_tipo memberships, plus any static_list memberships (hr.static_lists + hr.static_list_members) and dynamic_list memberships (evaluated by running each list's criteria from hr.dynamic_list_details against 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):

Strategyboolintstringjson
restrictive (default)AND — true only if all groups agreemin valuealphabetically firstintersection
permissiveOR — true if any group says truemax valuealphabetically lastunion

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)

TablePurpose
cloud.config_catalogKey definitions, types, defaults, level permissions
cloud.config_values_globalSystem-wide overrides
cloud.config_values_tenantPer-tenant overrides
cloud.config_values_groupPer-HR-group overrides
cloud.config_values_userPer-user overrides

HR schema (group membership sources)

TablePurpose
hr.static_listsNamed lists of employees (manual assignment)
hr.static_list_membersMembership join table
hr.dynamic_listsFilter-based group definitions
hr.dynamic_list_detailsIndividual filter criteria for dynamic lists

API endpoints

All config management endpoints are under POST /api/config/. See Config API.