vike
Concepts

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 date

The 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.

On this page