# Configuration (/product/infrastructure/configuration)



Copy comes from **messages**, layered base → app → tenant. Behaviour comes from three layers that differ by who owns the choice and where it is stored.

| Layer   | Word            | Owned by     | Stored in               | Read                      |
| ------- | --------------- | ------------ | ----------------------- | ------------------------- |
| tenant  | **settings**    | the merchant | the settings table      | server, cached and tagged |
| user    | **preferences** | a person     | reserved, nothing built |                           |
| browser | **persistence** | a device     | cookies, local storage  | server for cookies        |

## Messages

No raw text sits in a component, even in an app that ships one language. Strings live in JSON per namespace, so a copywriter or a tenant can change wording without touching code.

Three layers merge at request time, rightmost winning: the shared base every app ships, an app-local layer for one app's own identity, and the tenant's overrides from its own database, live without a redeploy. The shared form copy is folded into each module's own form node after the merge, so a tenant override of a field label reaches every form and a module's own copy still beats it.

Two surfaces edit the tenant layer. The copy admin lists every shipped key of a namespace with the shipped string as placeholder and the override as value; clearing a row restores the base. **Text edit mode** marks every string on an admin page with an invisible marker carrying its message path, so an admin can Alt-click any label in place and write the same override. The assistant writes the same layer through its own tool. All three go through one database function that merges per path, so an admin's edit and an assistant's write to the same namespace never replace each other.

Chrome is translated; catalog content is not. A product's name is single-language whatever the URL says.

Language is a URL segment, resolved once by the proxy from the URL, else a cookie, else the browser's preference. Both `/af/shop` and `/shop` render, the first canonical and indexable, the second following the cookie, and nothing redirects between them. Links stay language-free, and crawlers reach the prefixed URLs through the sitemap.

## Settings

The settings table is a key-value store: one row per scope, the configuration in a single JSON column. Application-wide rows, the app's currency and gateway, the shop, the marketing site, the chat, the theme, load as one bundle under one cache tag, the way messages do. Each has a schema, so a drifted row fails the one scope that reads it, loudly and by name, rather than every read for the tenant.

Everything a module configures lives in one row named after the module: its form surface and, per grid it owns, that grid's setting flags, column definitions and filter definitions. A `defaults` row is the fallback for any grid or module with no entry. A grid setting can be restricted to roles; the server resolves the raw flags against the user's role into plain booleans, so one configuration makes a grid richer for an admin and leaner for a standard user, and no component ever sees a role.

A tenant can author a column that picks a built-in renderer or a filter that names a reference list, but not one that reaches a bespoke cell or an async search. What only code can express stays code-authored. The assistant edits the same rows through tools that replace one node at a time, and every settings change is audited and reversible.

## Theme

Colour lives in its own theme row and injects at runtime: change the row and the store repaints on the next request. A tenant stores a **selection**, a named theme from the committed catalog plus per-token overrides, so fixing a catalog theme once reaches every tenant on it; a raw pasted sheet is accepted too. An app with no tenant runtime bakes a catalog theme into its own stylesheet at build time instead.

## Persistence

Per-device choices go in a cookie where the server needs to read them so first paint matches the last choice, and in local storage where only the client cares. Light or dark, a grid's view and sidebar state, its filters, sorts and column layout all live here, namespaced by the collection the surface is bound to.

The admin's per-request **mode**, view or edit, is a cookie the proxy decodes into a URL segment, so the server render can read it without a cookie read that would cost every route its static shell. Only a signed-in admin can arm edit; the proxy derives that from the session verdict, never from the cookie alone.
