# Customization layers Within Channel 1 (metadata-driven customization, see [customization channels](customization-channels.md)), xly supports a **layered overlay model**. A base form is defined system-wide; tenants and users overlay changes on top *without* modifying the base. The framework merges the layers at request time and returns one merged form to the SPA. For the *worked example* of how this plays out for a custom field, see [Slice 4](../slices/04-custom-field.md). This page is the canonical overview. ## The layers ```mermaid flowchart TB classDef sys fill:#e8f0fe,stroke:#4285f4 classDef tenant fill:#fef7e0,stroke:#fbbc04 classDef user fill:#f3e8fd,stroke:#a142f4 M["gdsconfigformmaster
system default — the form
(sSqlStr · sWhere · sOrder)"]:::sys P["gdsconfigformpersonalize
per-tenant whole-form override
(replaces sSqlStr / sWhere / sOrder)"]:::tenant S["gdsconfigformslave
system default fields"]:::sys CS["gdsconfigformcustomslave
per-tenant fields
(add · hide · override by sName)"]:::tenant US["gdsconfigformuserslave
per-user view tweaks
(column order · hidden columns)"]:::user OUT["Merged form
delivered to SPA"] M --> P P --> S S --> CS CS --> US US --> OUT M -. "always loaded" .-> OUT P -. "loaded if tenant has overlay" .-> OUT CS -. "loaded if tenant has overlay" .-> OUT US -. "loaded if user has prefs" .-> OUT ``` Read the chain top-to-bottom: **system → tenant → user**. Each layer is keyed by `sParentId` linking up to the layer above. None of the links are FK-enforced — see [no-FK reality](semantic-fk.md). ## What each layer answers | Layer | Scope | Answer to | |---|---|---| | `gdsconfigformmaster` | system-wide | "What does this form look like by default?" | | `gdsconfigformpersonalize` | per-tenant | "Does this tenant want a different SQL/where/order?" | | `gdsconfigformslave` | system-wide | "What fields does the form have by default?" | | `gdsconfigformcustomslave` | per-tenant | "Does this tenant want extra fields, or a hidden field, or a relabelled field?" | | `gdsconfigformuserslave` | per-user | "Has this user reordered or hidden columns in *their* grid?" | ## How the merge happens The framework reads each layer in order and merges by `sName` (the field name). For a custom slave row with the same `sName` as a base slave: override. For a new `sName`: append. For a base slave with no corresponding custom row: pass through unchanged. Entry point is `BusinessBaseServiceImpl.getModelBysId` (line 181), which calls `BaseServiceImpl.getModelConfigByModleId` (line 55); the actual slave-+-customslave merge runs in `BusinessGdsconfigformsServiceImpl.getGdsconfigformslaveShow` (line 392), combining `getFormSlaveData` (line 87) and `getFormCustomSlaveData` (line 121), then optionally layering `getUserFormSlaveData` (line 156). Two database **views** support the merge by joining the form-master with the relevant slave table: - `gdsconfigformslavemasterview` — base layer - `gdsconfigformcustomslavemasterview` — overlay layer These views are read directly by the service code; the merge logic is in Java rather than SQL. ## What the layers can't do The overlay tables let a tenant **add fields to the form**, but they do not (and cannot) `ALTER TABLE` the underlying physical table. A custom field beyond what the base table provides has nothing to bind to on save unless one of the following is true: 1. The base table is intentionally "wide" (has spare columns reserved for customization). 2. A coordinated manual schema migration adds the column. 3. The customization is purely view-related (the field is computed or displayed, not stored). Case (2) is the typical engineer-led path. Case (1) explains some of the schema's mysteriously over-broad tables. When the customization needs *structural* change beyond what overlays support, [Slice 5 (per-customer SQL overrides)](../slices/05-customer-sql-override.md) is the next channel up.