# ADR-0048: Site files and the `redirect` runtime

URL: https://scalws.com/docs/adr/0048-site-files/
Source: `docs/adr/0048-site-files.md` in the ScalWS repository

Status: accepted, 2026-10-12

## Context

Every site lived in `scalws.yaml` under `tenants:` → `applications:`, three levels deep,
next to listeners, limits and the rest of the server. Adding a domain meant editing that
file correctly; a `www.` redirect needed a second application with its own root and an
`.htaccess`. Operators coming from nginx or Apache expect one file per site
(`sites-enabled/`, `vhosts.d/`), and a tool that adds a site should not have to rewrite
the whole configuration.

## Decision

* `include:` (a path or a list) in the main file names site files: a file, or a
  directory and a file pattern with one `*` (`sites.d/*.yaml`), relative to the main
  file. Matches are read in name order; hidden files are skipped; the main file alone may
  include. A missing directory is an error, an empty one is not.
* A site file holds one site in a short form (`domain`, `aliases`, `www: none|alias|redirect`,
  `root`, `tls: auto|manual`, one runtime shorthand: `static`, `php`, `node`, `python`,
  `proxy`, `redirect` or the full `runtime`, plus `profile`, `routes`, `cache`,
  `security`, `htaccess`, and the tenant settings `tenant`, `user`, `resources`), or a
  `tenants:` list in the full syntax. Tenant and application names default to the domain
  (`shop.example.com` → `shop-example-com`). Sites of one tenant share it; its settings may
  come from one place only. `tls` defaults to `auto` when `server.acme` is enabled.
* The loader expands site files into ordinary tenants and applications before profile
  resolution and validation: the server, the optimizer and the controller see one
  `Config`. Messages about a site name its file (`sites.d/a.yaml: runtime.upstream: …`);
  a domain served twice is the existing duplicate-domain error.
* scalws-controller stores a configuration with `include` expanded (one self-contained
  document) as the generation, so a generation never changes when a site file does. A
  configuration without `include` is still stored byte for byte.
* On Unix a site file and its directory must belong to root (or the main file's owner)
  and must not be writable by group or others: writing a site file means serving any
  domain from any directory the server can read.
* `redirect` runtime (`to`, `status` 301/302/303/307/308, `keep_path`): the answer
  carries `Location: <to><request path and query>`; the target is an origin, so the
  request path cannot change the host. `www: redirect` uses it.
* `https_redirect: true` on an application (and in site files) answers plain-HTTP
  requests with a 301 to `https://<routed host><path and query>` after the routing and
  access checks; ACME HTTP-01 challenges are answered before it. One boolean test per
  request on the routed application.
* `scalwsctl site add|list|remove` writes `<domain>.yaml` (0640, the main file's group),
  refuses a domain that is already served, runs the reload check and removes the file
  again when it fails, then reloads (through scalws-controller when it runs). `remove`
  keeps the file as a hidden backup next to it.
* `scalwsctl migrate nginx|apache` turns the virtual hosts of an existing configuration
  into site files (read-only on the source); conflicts with served domains are asked about
  per domain, only a domain's own site file is ever replaced, and a run that fails the
  check or the reload is undone as a whole.
* The console validates drafts with the site files of the configuration's directory and
  stages the expanded document for `scalwsd --check`; its statistics include them.

## Consequences

* The package installs `/etc/scalws/sites.d` (root:scalws, 0750) and a default
  `scalws.yaml` with `include: sites.d/*.yaml`; existing configurations are unchanged
  (`noreplace`) and keep working without `include`.
* Site files are not edited by the console's forms yet; the console shows their sites and
  validates against them.
* Data-plane changes: the `redirect` handler (a fixed response with a `Location`
  header) and the `https_redirect` test.
