Settings
One directory, five files split by who is allowed to change a value — and the risk ceilings have no environment layer at all, because a ceiling you can raise from a shell export is not a ceiling.
Nearly every setting Vike Trader reads lives under one directory, <project>/settings/, in five
files whose names say what kind of value each holds — a run profile's [risk] table, under
<project>/user_data/profiles/, sits outside it, and a live mount refuses to start without the two
caps it supplies:
<project>/settings/
├── secrets.env credentials — the one store, nothing else can supply a key
├── policy.toml hard ceilings — file only, no environment, no command line
├── config.toml deployment wiring — store paths, log directory, listen addresses
├── preferences.toml taste and tuning — log levels, chart style, sweep threads
└── flags.toml operator toggles, each with an owner and a review dateThe split is by authority — who is allowed to change a value and from where — not by
subsystem. crates/vike-config/src/lib.rs is the typed model and layered loader for the four TOML
files; crates/vike-secrets/src/lib.rs owns the credential store. The decision record behind the
shape opens with the problem it solved: settings had lived in eight mechanisms with no root and no
precedence order, and max_leverage was exactly as env-overridable as a chart colour.
Policy has no environment layer, by construction
The precedence is one line and applies to everything: code defaults, then the file, then the
environment, then the command line. What differs per type is which layers exist. Policy —
leverage, per-order notional, the market-slippage band, halt admission, and the per-venue
arming ceiling — is the one settings type that implements
neither EnvOverride nor CliOverride. crates/vike-config/src/layers.rs seals both traits, so
settings.policy.apply_env(&env) is not a discouraged call but a compile error, and a downstream
crate cannot bolt the impl on from outside. The command-line layer has no policy field either,
because arguments leak into process listings, shell history and CI logs. Anyone who can set an
environment variable on a box — a shell export, a stale service-unit line, an inherited parent
process — can no longer widen a live risk limit with no file changed and no diff.
A removed setting is a hard startup failure, and that is the safe direction
When the per-order notional ceiling moved from two environment variables into
policy.max_notional_per_order, deleting the reads was only half the job: an operator who still
set the old variable believed a ceiling was armed, and a build that quietly ignored it would trade
uncapped while they believed otherwise. So every settings-loading binary calls
vike_config::refuse_removed_env first — before a window, a feed, a core or a venue mount — and a
set, non-empty removed variable refuses to start, naming the file, the key, and the operator's own
value as a paste-ready line.
A present-but-empty value starts normally: it configured nothing under the old readers either.
The same rule covers a removed file: a <project>/vike.toml override layer existed for one hour
and is refused by name if found — and a path that cannot be probed refuses too, but says so rather
than claiming a file is present.
A key nothing reads is worse than a missing feature
A settings key that validates, is accepted by the loader, and is reported by vike-cli config show
as the origin of an effective value — while being read by nothing — hands the operator positive
confirmation of something false. crates/vike-config/src/consumed.rs records that it is not
hypothetical: a clean-install validation found flags.tradehub_control, config.tradehub_addr,
config.log_dir, config.state_dir and preferences.log_file_level all displayed as effective
while read by nothing — no control server, the log in the wrong directory, and a trace firehose
the operator believed they had capped, which once wrote 341 GB onto the disk hosting a live trading
node. vike_config::CONSUMPTION is the answer and the ratchet: one row per config, preferences
and flags key naming the file and the read itself, or a written admission of which reader still
owns the variable today. A test opens each claimed consumer and looks; a needle inside the config
crate is rejected, because parsing a field is not consuming it. config show's READ column
prints the verdict per key, and names the binary where one program owns the read (a library
consumer, read by every binary that links it, prints yes instead). The policy ceilings have an
older twin of the same gate; both are on settings gates.
Provenance is measured the same way: crates/vike-config/src/provenance.rs decides an origin by key
presence in a parsed layer, never by diffing values against defaults. Unknown keys are rejected
by name, a removed key is a tombstone whose error says where the concept lives now, and a truthy
typo on a flag (VIKE_RECONCILE=true) is an error rather than a silent false. Every flag defaults
to false, and FLAG_REGISTRY gives each an owner and a review date, gated in CI — a flag with no
owner is a flag nobody will ever delete.
Where the project is
<project> is resolved at runtime by walking up from the working directory for a project marker —
never a compile-time path — and VIKE_SETTINGS_DIR beats the walk by naming <project>/settings
outright, the project being its parent; the shipped service units set it. The loader itself
resolves nothing: the settings directory and the environment map are parameters, so the whole load
is a pure function of its arguments, and a missing file is not an error — the layer below stands.
The three binaries that load settings do so before initialising logging, because the log
directory and both log levels are themselves settings. The
architecture page carries the rule that one walk decides per
process.
Credentials
secrets.env is the one store: no precedence, no second location, so "which file are my keys
coming from" has a one-line answer. Absent credentials are the live gate — no store means an
empty map, every venue loader returns None, and every venue stays paper. A store that exists
and cannot be read is the opposite case and errors, because "not configured" and "cannot open"
must never look the same to an operator. A permission finding on a group- or world-readable file
is reported, never enforced: refusing would strand somebody mid-setup with every venue on paper,
while reading it silently is worse still. Nothing ever regenerates, reorders or deletes the store;
the one sanctioned write is a byte-preserving upsert of named keys.
Files, not a database — and a journal beside them
The question "should settings live in SQLite, since they change often?" was asked and measured.
Across the four TOML files there were three non-empty settings lines in total. The decisive
argument against an embedded store is that the change journal has two live writers — the daemon
and the desktop app — and an exclusive file lock would make the second fail to open it. So settings
and credentials stay files, and the change history the question actually wanted is an append-only
JSONL journal beside them, vike_model::change_journal, under <project>/settings/state/changes/.
The record it replaces was a tracing line at info on a daemon whose file level was warn: a
23 MB log held 53 160 error lines, 1 928 warnings and zero of the records.
Each file's keys, with their consumers, are on the settings reference; the removed variables and the registry of every environment read are on environment variables.
The latency gate
The execution core's fold is measured in CI on reserved cores and gated on its tail, not its median — and only three of the harness's variants gate on a derived budget, because an uncalibrated ceiling is worse than no gate at all.
The event bus
A defer-and-deliver FIFO whose delivery order is load-bearing, whose handler reports whether it actually folded an event, and whose drain guard salvages the event a panic was holding.