Part 1
Configuration in levels, one precedence
Study first
- The split: schemas say what exists, configuration says what this deployment does, code says how configuration is interpreted. A prompt with the company's name in it is code that is really configuration; a YAML with nested conditionals is configuration that is really code.
- Levels: the product's defaults, a tenant's overrides, the environment, a user's preferences, with the precedence written once. Every level is parsed at startup, so a wrong file or variable fails at startup, with the field or the pair named, rather than at the first request. The rules file is parsed at the same startup and is not a level: it has one source.
- What the client may know: a client bundle cannot read a file chosen at startup. The server reads the configuration; a client component gets what it needs as props, and a schema that depended on the customer becomes a function of what the server passes.
Build
content/customers/<id>.json, one per customer, withsettingsfor what it overrides;content/defaults.jsonfor the routes and the age a cached page is kept, which the product starts from;CUSTOMERinsrc/lib/config.tsnaming the file a deployment serves.src/lib/settings.ts: theSettingsandOverridesschemas, andresolve(defaults, overrides, env), the precedence in one place: a customer's value replaces the default's, field by field; a route fromROUTESgoes first among the models its task tries.src/lib/routing.tsreads the resolved routes and nothing else.src/lib/request-model.tsloses its import of the customer:requestModel(teams)builds the schemas for a list of teams,RequestDraftis what a client composes, andsrc/lib/requests.tsbuilds the real ones from the customer on the server. The catalog takes the teams the same way.- A second tenant from a file:
content/customers/harbor-dental.json, with its own teams and assistant, andsettings.routes.chatoverriding the chat's models. The assistant's name reaches every page from the file, and the chat, a client component, takes it as a prop: 12 places had said WorkBot in code. - Tests: the precedence, level by level; a bad route or task refused at load; a run under
CUSTOMER=harbor-dentalthat checks the name, the teams, the prompt and the overridden route in one go.
Done when
CUSTOMER=harbor-dental pnpm build && pnpm startserves Harbor Dental's assistant with no change to code.

Technical notes
- The refactor touched 14 files, nearly all import lines: modules that read
teamsfrom the model read it fromrequests.ts, which builds the model from the customer. One action changed shape: the catalog'ssubmit_requestkeeps its team check only when it is built with the teams, so the client's registry, which never prompts a model, builds it without, and the API checks the team. - The eval workflow's trigger paths name
content/customers/**,content/defaults.jsonandsrc/lib/settings.ts: a prompt is a function of the customer, and a route decides the model. CUSTOMERis read when the app starts, and a page prerendered at build carries the build's customer, so a tenant is a build. The first attempt served the second tenant from the first tenant's build, and the brand on screen said so.- Bringing Harbor Dental up found the routing rules naming teams it lacks, and the app refusing to start on them. The rules split: the product's, which name no team, in
content/rules.rules, and a customer's beside its file, read after them. - User preferences, the brief's third level, are not here: there are no users yet.


