Configuration¶
Everything that tunes a Nerthus deployment without a recompile lives under .nerthus/ at the repo root. There are three kinds of knob: the daemon config (config.json — read once at boot), the Council-editable data tables (data-tables/ — read on each use), and the tunable PU constants (the pu block of config.json). The client never reads any of them — it asks the daemon, whose public schema route serves the effective table contents. This page is the lookup contract for file locations, shapes, defaults, and precedence; what each table means is owned by its domain page and linked from here.
Committed vs. private¶
| Location | Tracked | Contents |
|---|---|---|
.nerthus/config.json |
committed | Daemon config + PU constants — Council policy, reviewable diffs. |
.nerthus/data-tables/ |
committed | Council lore tables: currency.json, roles.json, towns.json. |
.nerthus/runtime/ |
gitignored | Machine-managed: tokens, discovery files, sync tick stamp. |
.nerthus/local/ |
gitignored | Operator-managed host-local files (sync.json). |
.nerthus/cache/, log/, state/ |
gitignored | Refetchable machine state, JSONL streams, governance state. |
The full .nerthus/ layout is owned by Architecture. Nerthus never hardcodes a secret into a tracked file; the one deliberate exception — per-player webhook URLs in the committed index — is recorded under Secrets.
.nerthus/config.json¶
One committed JSON object, merged key-by-key over the daemon defaults — an omitted key falls back; an unknown key is merged but nothing reads it. A fresh scaffold writes exactly {}; every key below is added by hand when the Council changes a policy, so the file stays diff-friendly. Canonical shape (the values shown are the built-in defaults):
{
"server": { "bind": "127.0.0.1", "port": 0, "read_only": false,
"sync_max_age_min": 60, "sync_branch": "" },
"integrations": {
"discord": { "enabled": true, "rate_per_minute": 30, "jwks_url": "https://discord.com/api/oauth2/keys" },
"logs": {
"hosts": ["krisaphalon.ct8.pl", "pastebin.com", "docs.google.com", "drive.google.com", "imgur.com"],
"fetch_timeout_ms": 15000,
"failed_ttl_hours": 24
},
"margonem": {
"profile_url": "https://www.margonem.pl/profile/view,",
"validate_url": "https://public-api.margonem.pl/account/validate",
"signing_key_url": "https://staticinfo.margonem.pl/.well-known/signing-key.pem",
"signing_key_cache_ttl_s": 86400,
"auth_timestamp_skew_s": 300,
"session_ttl_s": 14400,
"map_checkup": {
"enabled": true, "interval_hours": 24,
"request_delay_ms": 1000, "timeout_ms": 10000, "max_probe_steps": 5,
"failure_budget": 20, "batch_size": 300, "tick_budget_ms": 4000,
"retry_cooldown_ms": 30000,
"hosts": ["micc.garmory-cdn.cloud"]
}
}
},
"pu": {
"monthly_base": 1.0, "monthly_cap": 5.0,
"election_window_months": 6, "election_threshold": 3.0,
"new_character": { "divisor": 2.0, "bonus": 20.0, "floor": 20.0 }
}
}
server¶
| Key | Type | Default | Meaning |
|---|---|---|---|
bind |
string | 127.0.0.1 |
Ignored — the listener always binds loopback, regardless of this value; the key documents intent. |
port |
int | 0 |
0 = OS-assigned ephemeral port (the chosen value is published to runtime/daemon.port). Pinnable here or via the boot script's -Port, which wins. |
read_only |
bool | false |
Boots the daemon in read-only mode — every mutating route 403s at the write gate. The boot script's -ReadOnly switch forces the mode regardless of this key; Set-NerthusMode flips it at runtime without editing the file. |
sync_max_age_min |
int | 60 |
Staleness bound (minutes) for the SyncStale write-gate check on a sync-enabled instance. |
sync_branch |
string | "" |
The branch the git sync converges and publishes; empty auto-resolves from origin/HEAD. |
The two sync_* keys tune the in-daemon git sync but can never enable it — enablement is host-local (local/sync.json or a bound boot parameter; see Precedence), so both keys are inert on every non-syncing clone. The sync subsystem itself is owned by Sync.
When the daemon is published behind a Cloudflare Tunnel or a reverse proxy, pin port (or the boot script's -Port, which wins) so the edge ingress has a stable loopback target. bind still stays loopback: the tunnel co-locates on the VM and reaches 127.0.0.1:<port>, so nothing binds a public interface (Publish the API).
No logging block¶
There is no logging config block. The three JSONL stream names under .nerthus/log/ — operational, request, audit — are fixed in code, the streams are append-only with no rotation, and request bodies are never logged (see Logs). Earlier drafts declared level, retention_days, and request_body keys; they were dropped as unenforced promises — a future consumer reintroduces its key together with the code that reads it.
integrations.discord¶
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
false silences all webhook sends; the PU batch then skips its notifications. |
rate_per_minute |
30 |
Reserved — no throttle consumer yet. |
jwks_url |
Discord keys URL | Where the Discord OIDC verification keys are fetched. |
Delivery detail lives in Logs. Per-player webhook URLs never live in config — they are @prfwebhook tags on the Gracz blocks of the committed index.
integrations.logs¶
| Key | Default | Meaning |
|---|---|---|
hosts |
see shape above | Allow-list of hosts the game-log fetcher may reach. |
fetch_timeout_ms |
15000 |
Per-fetch timeout. |
failed_ttl_hours |
24 |
Retry window for .failed markers; 0 disables auto-retry. |
Retry semantics and the fetch/parse pipeline are owned by Logs.
integrations.margonem¶
Endpoints and TTLs for the Margonem auth flow, whose mechanics are owned by Permissions.
| Key | Default | Meaning |
|---|---|---|
profile_url |
Margonem profile URL | Records the flow's external profile endpoint; the daemon never calls it. |
validate_url |
Margonem validate URL | Records the flow's external validation endpoint; the daemon never calls it. |
signing_key_url |
Margonem key URL | The public signing key the daemon fetches and caches. |
signing_key_cache_ttl_s |
86400 |
Cache lifetime of the fetched signing key. |
auth_timestamp_skew_s |
300 |
Payload freshness window for signed auth payloads. |
session_ttl_s |
14400 |
Minted session-token lifetime. |
integrations.margonem.map_checkup¶
The CDN version sweep over Mapa @url values — Locations owns its contract, the schedules Sync and Set up pipelines.
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
Gates the serve-loop tick only — the explicit route always answers. |
interval_hours |
24 |
Tick cadence. |
request_delay_ms |
1000 |
Spacing between HEAD probes. |
timeout_ms |
10000 |
Bound on each probe. |
max_probe_steps |
5 |
Cap on how many versions past the current one a sweep walks. |
failure_budget |
20 |
Aborts the sweep after that many consecutive map errors (a dead CDN). |
batch_size |
300 |
Cap on a tick's map count. |
tick_budget_ms |
4000 |
Cap on a tick's wall-clock. |
retry_cooldown_ms |
30000 |
Wait before the single retry after a network error. |
hosts |
["micc.garmory-cdn.cloud"] |
Probe allow-list. |
Behavior notes: a 404 is the normal "no update" answer and never counts against the budget; scattered throttle hits only flag their maps. batch_size and tick_budget_ms — whichever is hit first ends the chunk, so the single dispatch thread never stalls longer than one chunk. The retry cool-down is sized to ride out the CDN's sliding rate-limit window. A Mapa @url on a host outside hosts is skipped, mirroring the log-fetch SSRF rule; an explicitly empty list probes nothing (only a missing key falls back to the default host). Probes never follow redirects: a redirecting CDN front reads as errors that spend the budget, never as freshly published versions.
Tunable PU constants (pu block)¶
Council policy for the monthly PU batch and election eligibility; the full algorithm is owned by PU model.
| Key | Default | Meaning |
|---|---|---|
monthly_base |
1.0 |
Universal +1/month every active character receives on top of session grants. |
monthly_cap |
5.0 |
Maximum PU credited to SUMA in one month; the remainder spills to NADMIAR. |
election_window_months |
6 |
Rolling window for election eligibility. |
election_threshold |
3.0 |
Windowed score below which a Gracz is ineligible. |
new_character.divisor / .bonus / .floor |
2.0 / 20.0 / 20.0 |
The new-Postać STARTOWE baseline formula — see PU model. |
Council data tables (.nerthus/data-tables/)¶
Three committed lore tables the Council edits directly: currency.json (denominations), roles.json (role → capability bundles), and towns.json (reputation settlements). The seeded shapes, row semantics, and the shared lifecycle rules (read on use, seed only when absent, fallback to baked-in defaults) are in Data tables.
The public schema route serves the effective contents — statuses, tags, entity types, denominations, towns, role names, and the capability catalog derived from the route table and role bundles. The PU constants and the full role bundles stay off that route.
Precedence¶
Three tiers, highest wins — deliberately flat:
- Request parameter — a value passed on the call overrides the file for that operation:
Set-NerthusMode -ReadOnlyflips the running daemon without touchingserver.read_only; the daemon boot script's-Portbeatsserver.port. Runtime overrides are never written back toconfig.json. - Committed config —
config.jsonanddata-tables/as checked in. - Daemon defaults — the built-in values shown above, used for any omitted key. A fresh scaffold writes
config.jsononly when absent, as exactly{}.
There is no environment-variable tier and no user-profile tier; host-specific secrets are the only thing outside the committed files, resolved by reference, not by precedence.
Note
Git-sync enablement never enters the three tiers. It resolves from host-local sources only — a bound boot parameter (-SyncIntervalMinutes, -SyncPublishMode), else .nerthus/local/sync.json, else disabled; the committed config.json cannot participate. A bound -SyncIntervalMinutes 0 force-disables sync for that boot even when sync.json enables it — the operator's one-boot escape hatch. The two committed sync_* tunables keep the normal order beneath those sources (Sync).
Secrets, runtime & host-local files¶
Secrets never live in tracked files, with one deliberate exception: per-player Discord webhook URLs are @prfwebhook tags on the Gracz blocks of the committed index (Logs owns resolution and delivery). That is an owner decision — the retired Gracze.md carried the same URLs in git, so the exposure class is unchanged. Everything else sits in the gitignored .nerthus/runtime/ (machine-managed) and .nerthus/local/ (operator-managed):
| File | Notes |
|---|---|
runtime/daemon.token |
The machine token — mode 0600, full capability, minted at boot. |
runtime/daemon.port, runtime/daemon.pid |
Discovery files the client reads to find the daemon. |
runtime/tokens/ |
Session and named token records, stored under the token's SHA-256 id — a raw bearer value is never written to disk (see Permissions). |
runtime/sync.json |
The sync tick stamp: observability plus the cross-restart seed for the publish-retry state; the write gate never reads it (Sync). |
local/sync.json |
The one host-local config file — git-sync enablement, shape below. |
local/ |
Operator-managed host-local files. The webhook extract local/secrets.json is retired — the daemon never reads or writes it; a leftover copy is inert and safe to delete. |
local/sync.json is not a secret but is deliberately un-committed. Its shape: { "intervalMinutes": 5, "publishMode": "push" } — every boot path reads it, the client auto-spawn included, and the daemon is a syncing instance exactly when the resolved interval is at least 1. Enablement can never travel through the committed config.json: a committed interval would turn every clone of the repo — every home and CI daemon — into a syncing instance whose writes freeze behind failing fetches. Behavior is owned by Sync; the setup procedure by Deploy the VM.
The Margonem signing key cached at cache/signing-key.pem is a public verification key, not a secret — it lives under gitignored cache/ only because it is refetchable machine state. A missing webhook (@prfwebhook: BRAK, or no tag) disables the affected delivery with a SKIP recorded in the delivery log, never a daemon crash.
schema.json pointer¶
.nerthus/schema.json ({ "version": "0.8.0" }) is a pointer-only file: the single index-format version the on-disk data satisfies. It is a separate file from config.json and unrelated to it. The one-pass import is its sole writer — it stamps 0.8.0 directly — and the write gate reads it; the lifecycle is owned by Adoption.
Cmdlet surface¶
Configuration is read-mostly; there is no "set arbitrary config" route — the Council edits files. The runtime-facing cmdlets are Start-Nerthus, Stop-Nerthus, Get-NerthusStatus, Get-NerthusSchemaVersion, Set-NerthusMode, and Initialize-NerthusRepo; routes, parameters, envelopes, and capabilities are in API. config.json is read once at daemon boot — apply an edit with Stop-Nerthus + respawn. The data-tables/*.json files are read on each use, so a table edit (a new denomination, a rebound role) takes effect on the next request with no restart.
See also¶
- Architecture — the
.nerthus/layout, data-owner contract. - API — the cmdlet ⇄ route mapping, envelopes, capabilities, the write gate.
- Data tables — the seeded shapes of
currency.json,roles.json,towns.json. - Capabilities — the capability catalog the role bundles grant.
- PU model — how the tunable PU constants are applied.
- Currency model — the denomination table in use.
- Permissions — roles, capabilities, tokens.
- Logs model — log streams, game-log fetch, webhook delivery.
- Adoption — the
schema.jsonlifecycle and the one-pass import. - Sync — the git-sync subsystem the
sync_*keys andlocal/sync.jsonconfigure.