Settings
Two halves, one page. The line between them is not infrastructure on one side, preferences on the other — that is an intention, and it decides nothing about a port number. The line is a test you can apply name by name:
What stays in the environment is what the process must know before it can open the store. Everything else lives in the store.
What you set in the app
The dials live in the store and nowhere else. There is no environment form for them, no precedence rule, no seed on first boot, no settings file: exactly one place says what a setting is worth, and that place is the one the app shows you. A key that has never been written is not a hole — it reads as the default written in the code, which is what lets a later version add a dial without migrating anything.
None of them requires a restart. A dial that would have needed one was deleted rather than moved, so the settings form has a single class of field.
| Dial | Default | What it decides |
|---|---|---|
base_currency | none | the currency every figure is reported in |
regular_interval | 120 | seconds between two polls of a symbol whose market is open |
backfill_interval | 60 | seconds between two runs of the rebuild job |
backfill_delay | 10 | seconds of politeness between two requests to Yahoo! Finance |
backfill_chunk_days | 365 | days of history fetched per request |
staleness_horizon | 900 | seconds before a frozen price is reported as stale; 0 disables the check |
That is the whole list. You turn them on the Settings page of the app, or with one call on the write API, which is always served on the same port as the page.
The base currency
It is the one setting with no default, and the only thing the app ever asks you. It has no default on purpose: a default would silently interpret every amount you have already recorded, so not answered yet and answered have to stay two different states.
It is changeable while your ledger is empty and fixed from your first event. With no event there is no held symbol and therefore no stored price, so nothing has been interpreted and nothing can be reinterpreted; what is unrecoverable is reinterpreting amounts, never answering late or thinking again beforehand.
Until it is answered, nothing refuses: prices are still fetched in their native currency, nothing is converted, and no performance figure is written at all — not zeros, not blanks. The app says so on screen rather than showing you a number it cannot justify. An import that declares its own currency sets it, which is what lets an install answer the question without a single call.
Two of these are not decorative
Changing regular_interval reaches only the symbols whose market is open
right now. Each symbol schedules its own next poll; a symbol whose market is
closed is asleep until it reopens and reads the new value when it wakes. So the
change is not portfolio-wide the instant you save it, and nothing is broken when
it is not.
It also rescales, retroactively, the back-off of a symbol that is already
failing. When a symbol stops returning a usable price, the wait between
attempts grows as a multiple of this interval — regular_interval × 2^(n−3)
after the third consecutive failure, capped at 24 hours — rather than as an
absolute delay stored somewhere. Lowering the interval therefore shortens the
wait of a symbol that has been silent since this morning, and raising it
lengthens it. No interface can hide that, because the number in the form is the
number in the formula. How prices are collected
has the rest of the back-off.
What the container imposes
Three names. This is the complete list of what the app reads from its environment; nothing else in it is consulted.
| Variable | Default | What it decides |
|---|---|---|
SB_STORE_DIR | /data | the directory the store lives in |
SB_WEB_PORT | 8080 | the socket the app is bound to |
LOG_LEVEL | INFO | how loudly the app logs |
- The path is a directory, never a file. The app names its own store file inside it. That removes the entire class of mistake where you point at a file whose parent directory is not mounted.
- A variable that is set and no longer read is named at start-up, in one
grouped line, and obeyed by nothing.
SB_IMPORT_DIRis one of those: the folder it named is gone, and a file is handed to the app instead. - The defaults describe the container, and it is the deployment without
Docker that overrides them — see
Install without Docker. A plain
docker runsets none of these. - A blank value counts as unset. An undefined substitution renders as an empty string, and an empty string is not an answer.
- The port passes the test twice. It is read in the server's master process before the app is even imported, so it cannot come from the store; and a port changed from the interface would cut the connection the interface arrived by. That is a nature, not only a sequence.
- There is one socket, and everything is behind it. The page, the reads and
the writes —
POST /api/events,PUT /api/settings,DELETE /api/store/orphansincluded — and/healthall answer onSB_WEB_PORT. There is no second port to publish and no port that serves a filtered view of the app. LOG_LEVELis here because the most likely failure of this app is the store failing to open, and a log level kept inside the store cannot report that.- No secret lives in the environment any more. There was exactly one, the database token, and it has no subject left.
There is no switch that turns a half of the app off
There is no variable that disables the interface, and there will not be one.
There used to be a second one — /metrics, on a socket of its own — and it is
gone: one owner, one browser and one container paid for a second interface
twice, once to build it and once in the shape it forced on the model. What is
left is not a smaller version of it; it is the app's own pages, and they are
where an installation now reports on itself.
So publishing the port publishes everything. Whoever reaches it reaches the
page, the reads and the writes — POST /api/events and PUT /api/settings
included. There is no port that is a filtered view of another, because there is
no other port. If the app must not be reachable from a network, that is a
decision for the network: bind the container to 127.0.0.1, or put it behind
something that authenticates. The app itself has no half to serve.
What version 4 said, and is no longer read
Every SB_* or INFLUXDB_* variable that is present in the environment and is
not one of those above is named at start-up and never obeyed. It produces
one grouped notice, not one line per variable, saying which names were found
and that these settings now live in the app. The list is computed — what is
present, minus the boot variables, minus the dials — so it cannot drift out
of date the way a hard-coded list of names would.
| No longer read | Where it lives now |
|---|---|
SB_REGULAR_INTERVAL | the regular_interval dial |
SB_BACKFILL_INTERVAL | the backfill_interval dial |
SB_BACKFILL_DELAY | the backfill_delay dial |
SB_BACKFILL_CHUNK_DAYS | the backfill_chunk_days dial |
SB_STALENESS_HORIZON | the staleness_horizon dial |
SB_INGESTION_INTERVAL | nowhere — there is no polling left: a replay follows the write that changed the ledger |
SB_SCRAPING_INTERVAL | nowhere — already deprecated in version 4, and its heir is a dial |
SB_PERF_INTERVAL | nowhere — the performance series is a cache, rebuilt in full every two minutes; there is nothing left to trade off |
SB_DYNAMIC_EXECUTOR_POOL | nowhere — sizing is always automatic |
SB_EXECUTOR_POOL | nowhere — same reason; it was the one dial that needed a restart, so it was deleted rather than moved |
SB_CONFIG_MODE | nowhere — there is one mode, and files are no longer the truth |
INFLUXDB_HOST | nowhere — the store is embedded |
INFLUXDB_TOKEN | nowhere — the app has no secret to be given |
INFLUXDB_DATABASE | nowhere — same reason |
SB_VERSION, SB_CONFIG_DIR, SB_UID and SB_GID are a different case
again: they carry the prefix but were never read by the app. They belonged
to the Compose file, and the Compose file is gone. They are not in the notice,
because naming them there would suggest the app once obeyed them.
.env.exampleThis inventory has two readers — this page and the app — and one source. A file of example variables would be a third writer of the same list, and the one nobody re-reads at release time.
Coming from a version 4 install? Coming from v4 covers what to do with the rest of it.