"Configurable" means three different things on this platform, and they are not interchangeable:
| kind | changed by | example |
|---|---|---|
literal |
a code deploy | the ceiling on an elevation's lifetime |
parameter |
a restart — an environment variable overrides the shipped default | whether elevation is enforced or only recorded |
setting |
a screen, while the system runs | how long an elevation lasts |
A literal is a value written into the code. A parameter is a container parameter with an environment override. A setting is a runtime value stored in the database and edited on a screen.
Every value is exactly one of these, decided once, and the page that describes the value says which. The three elevation values above are the worked example this page keeps returning to, because they sit one in each kind and the reasons are the point:
- The ceiling — one hour — is a literal, because a settable lifetime with no upper bound lets someone set a day, and "temporary" stops meaning anything while the mechanism still reports itself as working. A value nobody should change is a value nobody can change without a deploy.
- The mode —
COOLMS_ELEVATION_MODE,enforceorlog_only— is a deployment decision. It is read once, when the container is built, and not per request; an installation does not flip between rehearsing a rule and living under it from a screen. - The lifetime is a runtime setting, because it is the kind of number that
gets revised after living with it — fifteen minutes is too short, people
re-enter the password three times an hour — and a parameter that needs a
restart to change is a parameter nobody changes. Its environment variable,
COOLMS_ELEVATION_LIFETIME, is the default the setting starts from, not a second place to set it.
The rest of this page is about the third kind.
A module declares a block, and the declaration is the whole key set#
A runtime setting belongs to a block — identity.elevation,
identity.password, a cache block, a module's own — and a module ships the
block's declaration as a file: every key it will ever read, with a default
for each, null included. That rule looks fussy until you see what it buys.
The declaration is the key set: the reader has nothing to guess, the admin
screen is generated from it rather than hand-built, and a key that renders
blank is a key whose default was forgotten, which is a bug you can see.
The declaration is also an allowlist. The store behind settings holds other kinds of row — navigation trees, data grids — and the only thing stopping the settings endpoint from being write any row you like is that a write to an undeclared key is refused. So the declaration cannot itself be something a browser session writes; it is a file, read by a scanner, and changing it means filesystem access. That is an operator, not an admin, and it is the right line.
One reader owns the effective value#
A module never merges defaults with saved rows by hand. It asks one reader for the effective value of a block — defaults, overlaid by whatever has been saved — and that composition is done once, in one place, per request. The API returns the same composed map the module reads, so the admin screen and the running code cannot disagree about what a setting currently is.
Read per request matters for what a change means. The elevation lifetime is read at the moment an elevation is granted and written into that grant; the check that later asks "is this session still elevated?" reads the grant, not the setting. So saving a new lifetime affects the next grant and every one after it, and the elevations already running run out exactly as they were issued. A setting change is never retroactive.
What wins when two of them disagree#
Environment > saved row > shipped default.
A deployment that pins a value in its environment to ride out an incident must not be silently overridden by a row someone saved months ago. But precedence alone would reproduce a classic bug — a field an administrator can edit, save, and watch do nothing — so the other half is mandatory:
- A pinned key renders read-only, naming the variable. The screen says
set by
COOLMS_ELEVATION_LIFETIMEin the environment, and the person knows where to go. - A write that carries a pinned key is refused, with 422, rather than stored. The row would be inert; storing it would report a successful save for a change that never happens.
- Only the variable's presence is consulted, never its value. The pinned value is already the shipped default — the module declared it as a container parameter and the environment resolved it when the container was built — so restoring the default is applying the environment.
One nuance worth knowing before it surprises you: in development a .env file
populates the environment too, so a value set there reads as pinned. That is
correct — it is pinned in that environment — but the read-only field will
look wrong once until you remember why.
Platform-wide, or per site#
A block is platform-wide unless it opts into per-site values. A block that has opted in answers with the site's value where one is saved and the platform's otherwise, and the API exposes both levels:
GET /api/v1/module-settings every block this installation declares
GET /api/v1/module-settings/{key} one block, effective values
PUT /api/v1/module-settings/{key} save (a pinned or undeclared key is refused)
DELETE /api/v1/module-settings/{key} back to the shipped defaults
GET/PUT /api/v1/module-settings/{key}/sites/{site}
A scoped write to a block that has not opted in is refused: scope is part of the declaration, not something a caller asserts.
When a change takes effect#
| changed | it takes effect |
|---|---|
setting |
on the next read — the next request, or for the elevation lifetime, the next grant |
parameter |
when the container is next built: a restart, or a cache clear in development |
literal |
when the new code is deployed |
If a change does not seem to have happened, the first question is which of the three kinds the value is. The second is whether it is pinned. The screen answers both.
What is deliberately not here#
A reference listing every block, key, default and pin this installation has —
the counterpart to the console and
HTTP API pages, generated from the running application
so it cannot go stale — is planned and not yet built. Until it exists,
GET /api/v1/module-settings is that list, and it is authoritative because the
same reader produced it.