
# Multi-File Configs with `$ref`

One file stops being readable somewhere around the third table. `$ref` splits a YAML or JSON config across as many files as you like, with no change to the resulting object.

Any object whose **only** key is `$ref`, and whose value is a relative path, is replaced by the parsed contents of that file.

```yaml
# app.yaml
name: crm-workspace
version: 2.0.0

auth:
  $ref: ./config/auth.yaml

theme:
  $ref: ./config/theme.yaml

tables:
  - $ref: ./config/tables/companies.yaml
  - $ref: ./config/tables/contacts.yaml
  - $ref: ./config/tables/deals.yaml

pages:
  - $ref: ./config/pages/sign-in.yaml
  - $ref: ./config/pages/companies.yaml

agents:
  - $ref: ./config/agents/records-assistant.yaml
```

```yaml
# config/tables/companies.yaml
id: 1
name: Companies
fields:
  - { id: 1, name: name, type: single-line-text, required: true }
  - { id: 2, name: website, type: url }
```

## Rules

| Rule              | Behavior                                                                                                       |
| ----------------- | -------------------------------------------------------------------------------------------------------------- |
| Path resolution   | `$ref` paths resolve relative to the file that contains them, not the working directory.                       |
| Mixed formats     | A YAML root file may `$ref` a JSON partial and vice versa — each file is parsed by its extension.              |
| Array elements    | A `$ref` can stand in for a whole array element (`- $ref: ...`) or a whole object value.                       |
| Resolution timing | All `$ref`s are resolved into one object _before_ schema validation, so cross-section checks see the full app. |

Resolving before validation is what makes splitting safe: a rule such as "a record automation must reference an existing table" is still checked across the whole app, even when the automation and the table live in different files.

## The `config/` convention

`sovrium init` scaffolds — and the bundled templates follow — one shape: **one file per collection entity, one file per singleton, scalars stay inline.**

```text
app.yaml               # name, version, and the $ref map
config/
  auth.yaml            # singleton
  theme.yaml           # singleton
  tables/
    companies.yaml     # one entity per file
    contacts.yaml
  pages/
    sign-in.yaml
```

Nothing enforces this layout — `$ref` accepts any relative path. It earns its keep by making the root file a table of contents: what the app contains is legible without opening anything else.

## When to split

Keep a config in one file while it is small. Split once a section grows big enough to deserve its own name — in practice, past the first couple of tables or pages, or as soon as a theme gets substantial.

Validation attributes errors back to the file they came from, so a mistake in a partial is reported against that partial rather than against the root:

```text
Validation failed:
  companies.yaml: Unknown field type "web-site" in field "website"
```

:::callout
**`$ref` is YAML and JSON only.** TypeScript configs compose with ordinary `import`s instead — see [TypeScript Configs](/en/docs/configuration-typescript).
:::

## Related Pages

- [Config Files: YAML & JSON](/en/docs/configuration-files) — formats and resolution order.
- [Validating a Config](/en/docs/config-validation) — checking a split config in one command.
- [Templates & Examples](/en/docs/templates-examples) — real configs using this layout.
