Skip to content

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:

explicit request parameter  >  committed config.json / data-tables  >  daemon defaults
  1. Request parameter — a value passed on the call overrides the file for that operation: Set-NerthusMode -ReadOnly flips the running daemon without touching server.read_only; the daemon boot script's -Port beats server.port. Runtime overrides are never written back to config.json.
  2. Committed configconfig.json and data-tables/ as checked in.
  3. Daemon defaults — the built-in values shown above, used for any omitted key. A fresh scaffold writes config.json only 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.json lifecycle and the one-pass import.
  • Sync — the git-sync subsystem the sync_* keys and local/sync.json configure.