# 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<br/>system default — the form<br/>(sSqlStr · sWhere · sOrder)"]:::sys
    P["gdsconfigformpersonalize<br/>per-tenant whole-form override<br/>(replaces sSqlStr / sWhere / sOrder)"]:::tenant
    S["gdsconfigformslave<br/>system default fields"]:::sys
    CS["gdsconfigformcustomslave<br/>per-tenant fields<br/>(add · hide · override by sName)"]:::tenant
    US["gdsconfigformuserslave<br/>per-user view tweaks<br/>(column order · hidden columns)"]:::user

    OUT["Merged form<br/>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.
