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_type — bool, 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.