# 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. The merge happens
inside `BusinessBaseServiceImpl.getModelBysId` (line 181) and the
helpers it calls — `BusinessGdsconfigformsServiceImpl.getFormSlaveData`
+ `getFormCustomSlaveData`.
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.