Configuration
`.env`, `askr.toml`, and which one wins
# Configuration
Four layers, consulted in this order:
1. **Real environment variables**
2. **`.env`**
3. **`askr.toml`**
4. The default the caller passes
Keys are written with dots and looked up with underscores: `app.port`
becomes `APP_PORT` in the environment. That is the whole rule, and the only
part worth memorising is that **the environment always wins.** A deployment
must be able to set something without editing a file in the repository.
```pascal
uses Askr.Core.Config;
LoadConfig; { .env and askr.toml, found upwards }
Port := CfgInt('app.port', 8080);
Dsn := CfgOrFail('database.url'); { raises if missing }
Debug := CfgBool('app.debug', False);
```
`LoadConfig` is called once, first thing in `app.lpr`. It searches upward
from the current directory for both files, so commands work from a
subdirectory.
## The API
| | |
|---|---|
| `Cfg(Key)` / `Cfg(Key, Default)` | A string |
| `CfgInt(Key, Default)` | An integer, or the default if it does not parse |
| `CfgBool(Key, Default)` | `1/true/yes/on` and `0/false/no/off` |
| `CfgHas(Key)` | Present in any layer, even with an empty value |
| `CfgOrFail(Key)` | Raises `EConfigError` naming the key and where it looked |
| `CfgSource(Key)` | Which layer answered: `csEnvironment`, `csDotEnv`, `csToml`, `csNone` |
| `EnvNameFor(Key)` | `app.port` → `APP_PORT` |
| `ConfigFile` | Path of the `askr.toml` in use |
`CfgOrFail` names the key, the environment variable that would set it, and
the files it looked in — **never a value**:
```
Missing configuration "database.url". Set DATABASE_URL, or add it to
askr.toml. Looked in the environment, /srv/shop/.env, /srv/shop/askr.toml.
```
## `.env`
`.env` is where secrets live. Three rules, and they are not negotiable:
- **Real environment variables win over the file.** That is how production
sets values without the file existing, and it is how everyone else does it.
- **`.env` is never committed.** `.env.example` is. `askr new` puts `.env`
in `.gitignore`.
- **Values are never logged.** An error names the key that was missing,
never what it contained. `EnvOrFail` is written to be safe to leave in a
stack trace, and a test asserts that no value leaks into the message.
The format is the usual one:
```sh
SIMPLE=hello
export EXPORTED=yes
QUOTED="a b c"
ESCAPED="line1\nline2" # \n \t \" \\ are interpreted
LITERAL='raw \n stays' # nothing is interpreted
TRAILING=value # a comment
HASHPASS=pa#ssword # no space before # — part of the value
EMPTY=
```
A `#` without a space in front of it is part of the value, not a comment.
Otherwise a password containing `#` would be cut in half.
The file is read into the framework's own store, **not set with `setenv`**.
FPC's RTL keeps its own copy of the environment from startup, so `setenv`
would not reach a child process anyway, and a store we own is easier to
reason about than one we share with libc.
## `askr.toml`
The smallest thing that looks like TOML: `key = value`, one per line,
sections in brackets. No tables, no arrays beyond comma-separated strings,
no multi-line values. Needing more is a sign that the configuration has
grown past what it should be.
```toml
name = "shop"
main = "app.lpr"
frontend = "frontend"
units = "app,database"
watch = "app,database,frontend/src"
askr = "/path/to/askrcode"
[app]
port = 8080
backend_port = 8081
```
The parser is shared between the CLI and the app — `askr.toml` must not be
able to mean one thing to the tool and something else to the binary it
builds.
> **A `[section]` applies to everything below it.** `[app]` is written last
> in a generated `askr.toml` for that reason. In the middle of the file it
> would turn `units` into `app.units` and the build would stop finding the
> framework. That happened.
The app reads `[app] port` as `app.port`, which `APP_PORT` overrides.
## Environment awareness
```pascal
uses Askr.Core.Env;
AppEnv; { 'local' unless APP_ENV says otherwise }
IsProduction; { 'production' or 'prod' }
IsLocal; { 'local', 'development', 'dev' }
IsTesting; { 'testing', 'test' }
```
The default is `local`, not `production`. An app that *thinks* it is in
production without being there turns on things nobody asked for; the other
way around is noticed immediately.
`APP_ENV` changes exactly one thing by itself: the default log format is
text locally and JSON in production. See [Logging](logging.md).
## `app.url`, and why it is not the request
```pascal nocheck
uses Askr.Core.Url;
AppUrl { https://example.com, or '' }
AbsoluteUrl('/docs/queries') { https://example.com/docs/queries }
AbsoluteUrlOrFail('/reset/' + Tok) { the same, refusing when unset }
```
Every absolute URL an application emits names one origin: a link in a
password-reset email, and later a canonical link and a sitemap entry.
**That origin is configuration, and it is never taken from the request.**
`Host` is a header, which means it is text the client writes. A request
carrying `Host: evil.example` and a canonical built from it tells a search
engine the page lives on the attacker's domain; the same header in a reset
link sends the token there. Host-header injection is the ordinary name for
both.
So the functions above have **no way to see a request at all**. The
property is not that Askr remembers to ignore `Host` — it is that there is
nothing here to ignore it with. The end-to-end test drives a real socket
with a forged `Host` and requires the configured origin to come back.
`app.url` is an **origin**: scheme, host, optional port. A trailing slash
is normalised away; scheme and host are lowercased, because a URL differing
only in case is a second URL to a crawler. A path in it is refused rather
than dropped or kept — serving under a sub-path needs the router to agree
about the prefix too, and Askr does not do that yet.
`AppUrl` is empty when nothing is set, so a caller that can do without an
absolute URL leaves it out: a canonical link pointing at the wrong place is
worse than none. `AbsoluteUrlOrFail` is for where an absolute URL is the
whole point — a reset email with an empty `href` looks like a link and is
not one.
## Failing at startup, not on request 47
```pascal
RequireEnv(['DATABASE_URL', 'APP_KEY', 'SMTP_HOST']);
```
Checks all of them and raises **once**, listing every key that is missing —
never a value. One error at a time means as many restarts as there are
missing keys.
The point is the timing. Without it, a missing `DATABASE_URL` is discovered
on the first request that touches the database, possibly in production,
possibly as a 500 for a user.
## Seeing what actually applies
```sh
askr config # keys and sources, no values — safe to paste
askr config --values # values too, secrets still hidden
```
```
askr.toml /srv/shop/askr.toml
.env /srv/shop/.env
APP_ENV production
APP_ENV .env
DATABASE_URL environment
app.port askr.toml
```
`LooksSecret` decides what to hide: a key containing `secret`, `password`,
`token`, `key`, `credential`, `dsn` or `url`. It is not a definitive list
and cannot be one — which is why the default is to show no values at all.
## What is not here
A `config/` directory of source files, and a command to pre-compile it.
Askr's units
already take typed options records — `TServerOptions`, `TSessionStore.Create`,
`TDbPool.Create` — and a string-keyed `config('mail.from')` would be a step
down from something the compiler checks. The configuration layer is for
values that arrive from outside, not a replacement for parameters.