docs: operator console guide + fix example recipes + document all env vars

- New docs/operations/CONSOLE.md: reaching /admin, NIP-98 login, the
  defaults->env->DB precedence model, per-section runtime-safe vs
  restart-required table, expected_versions concurrency, restart_apply_mode
  (with the systemd Restart=always requirement for auto), clearing a DB
  override. Linked from README + OPERATOR_SETUP.
- All six example recipes gained an explicit chain-enablement block
  (FEATURE_XMR/WOW on, BTC/LTC/GRIN off) so they no longer hard-fail
  Config::validate in production (default-on chains with empty sources).
- Documented all 16 previously-undocumented env vars in .env.example
  (RESTART_APPLY_MODE, TOR_ADMIN_ONION, DEPLOYMENT_MODE, TIP_SHARE_BASE_URL,
  ...); RELAY.md gained the premium-post policy + RELAY_WRITE_ALLOWLIST_NPUBS;
  ADMIN_PUBKEYS noted as reserved/inert.
This commit is contained in:
jwinterm
2026-07-19 15:52:07 -04:00
parent fe2fc91cdd
commit a455c60ed0
11 changed files with 417 additions and 1 deletions
+85 -1
View File
@@ -13,6 +13,19 @@ SERVER_PORT=8080
# so every real deployment must replace it.
PUBLIC_API_URL=https://backend.example.org/api/v1
ENVIRONMENT=development
# Deployment topology: single (default) permits in-process challenge/nonce state;
# fleet means a load-balanced set of instances and requires shared/stateless
# challenge state. Must be single|fleet or boot fails.
DEPLOYMENT_MODE=single
# Comma-separated CIDRs or bare IPs whose X-Forwarded-For is trusted for the
# client IP (rate-limiting + audit). Empty (default) = always use the real TCP
# peer. Set this to your reverse proxy's address(es) when it terminates TLS.
TRUSTED_PROXIES=
# Comma-separated browser origins allowed by CORS (e.g. the web wallet). Empty
# (default) allows any origin — safe here because auth is a Bearer token, not a
# cookie, so no ambient credentials ride a cross-origin request. Credentials are
# never allowed.
CORS_ALLOWED_ORIGINS=
# ── Database ──────────────────────────────────────────────────────────────
DATABASE_URL=postgres://user:password@127.0.0.1:5432/smirk
@@ -36,6 +49,10 @@ FEATURE_POW=false
ALTCHA_HMAC_KEY=
POW_REQUIRED=false
ALTCHA_COST=100000
# Testing aid: comma-separated lowercase pubkey hashes that ALWAYS require PoW,
# even when POW_REQUIRED=false (opt-in, for exercising the gate). Requires
# FEATURE_POW=true or boot fails closed. Leave empty in normal deployments.
TEST_POW_REQUIRED_FOR_PUBKEYS=
# ── Registration gates (composable; each advertised via /capabilities) ────
# Require an operator-minted invite code to create a NEW wallet. Mint codes with
@@ -74,10 +91,32 @@ PAYMENT_EXPIRES_MINUTES=60
# enabled the two modes are identical.
REGISTRATION_GATE_MODE=all
# ── Chain enablement (each chain is independently feature-flagged) ─────────
# All default to TRUE. FAIL-CLOSED RULE: in production, an ENABLED chain with no
# source configured makes the server REFUSE TO BOOT — a chain that cannot be
# served must not advertise itself as available via /capabilities. So set the
# ones you do not run to false, or configure the source for each you keep on:
# BTC / LTC — an electrum/Fulcrum URL (BTC_ELECTRUM_URL / LTC_ELECTRUM_URL) or
# a fallback list.
# XMR / WOW — the *_LWS_ADMIN_KEY (plus its LWS + daemon URLs).
# GRIN — GRIN_OWNER_API_SECRET (plus the GRIN_* wallet/node wiring).
# In development these are warnings rather than a hard boot error.
FEATURE_BTC=true
FEATURE_LTC=true
FEATURE_XMR=true
FEATURE_WOW=true
FEATURE_GRIN=true
# ── Bitcoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ─────
BTC_NETWORK=mainnet
BTC_ELECTRUM_URL=
BTC_ELECTRUM_FALLBACKS=
# Require CA-verified TLS (webpki + hostname) for ssl:// electrum servers. Applies
# to BOTH BTC and LTC. Default false: public electrum servers are self-signed by
# convention and their data is treated as hostile regardless, so accepting any
# cert keeps the public fallbacks usable. Set true if you run your own CA-cert'd
# Fulcrum and want to harden.
ELECTRUM_STRICT_TLS=false
# ── Litecoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ────
LTC_NETWORK=mainnet
@@ -142,8 +181,26 @@ WALLET_RESTORE_POW_MAX_BITS=24
FEATURE_PRICES=true
PRICES_PROVIDER=coingecko
PRICES_FETCH_INTERVAL_SECS=300
# Which assets the price feed quotes. UNSET = all supported (btc,ltc,xmr,wow,grin);
# a subset like "btc,xmr" narrows it; PRESENT-BUT-EMPTY (PRICES_ASSETS= with no
# value) means none. Leave commented to serve all.
#PRICES_ASSETS=
# Currency the feed quotes in. One of: usd, eur, gbp, jpy, cny, aud, cad, chf, btc.
# A typo fails closed at boot (rather than serving an empty feed).
PRICES_CURRENCY=usd
# Nostr-native identity (NIP-98 login/link, NIP-05 directory). ON by default; when
# on, PUBLIC_API_URL is required (and must be https in production).
FEATURE_NOSTR_IDENTITY=true
# Grin slatepack relay: a non-custodial encrypted store-and-forward mailbox for
# interactive Grin transfers. ON by default; can be disabled independently of Grin
# chain access.
FEATURE_GRIN_RELAY=true
# Tips are parked for the conditional-transfer redesign; off until that lands.
FEATURE_TIPS=false
# Base URL for public-tip share links (a tip's share URL is {base}/{tip_id}).
# REQUIRED when FEATURE_TIPS=true (boot fails closed without it); never hardcode a
# host, so federated/self-hosted instances build their own share links.
TIP_SHARE_BASE_URL=
# ── Nostr relay (optional messaging plane; see docs/operations/RELAY.md) ────
# Off by default. When on, the operator runs a relay (the packaged
@@ -205,11 +262,21 @@ FEED_OWNER_NPUB=
FEED_ALLOWLIST_NPUBS=
FEED_EXTRA_RELAYS=
# ── Admin surface (see docs; default posture is loopback/Tor) ──────────────
# ── Admin surface (see docs/operations/CONSOLE.md; default posture is loopback/Tor) ─
# Admin auth is "Sign in with Smirk": only admin PUBLIC keys are stored here,
# never a seed or private key. The allowlist is mutated CLI/loopback-only in v1.
# When enabled, the embedded Operator Console SPA is served at /admin on this plane.
ADMIN_ENABLED=false
ADMIN_BIND=127.0.0.1:8081
# Allow ADMIN_BIND to be a NON-loopback address. Default false: boot REFUSES a
# public admin bind (confidentiality is by socket). Only set true if you front the
# plane with Tor client-auth or a trusted proxy and firewall it.
ADMIN_ALLOW_PUBLIC_BIND=false
# Tor onion host you reach the admin plane at (e.g. <hash>.onion). REQUIRED for the
# Tor-onion posture: it is added to the admin Host allowlist, so without it a
# request whose Host header is the onion is rejected 403 (anti DNS-rebinding).
# Never logged or surfaced publicly. Leave empty for the loopback/SSH-tunnel path.
TOR_ADMIN_ONION=
# Public absolute URL of THIS node's admin surface. The per-node id that binds
# signed admin actions (so a challenge signed for one node can't be replayed to
# another across a fleet) is DERIVED from this value — give each node in a fleet a
@@ -221,6 +288,17 @@ ADMIN_JWT_SECRET=
ADMIN_KEY_INTEGRITY_SECRET=
ADMIN_MAX_KEYS=8
ADMIN_PENDING_KEY_TTL_DAYS=7
# RESERVED / inert: ADMIN_PUBKEYS is parsed but NOT consumed anywhere in this
# version — it does not gate admin access. Authorization is the MAC-protected key
# allowlist in the database (managed via `smirk-admin` and the console Keys tab),
# never this variable. Leave it unset; setting it has no effect.
ADMIN_PUBKEYS=
# How the Operator Console applies a RESTART-REQUIRED config change (see CONSOLE.md):
# manual (default) — persist + flag; you apply it with a graceful restart.
# auto — the instance self-restarts after saving. REQUIRES a
# supervisor that brings it back (systemd Restart=always);
# otherwise it goes down and stays down.
RESTART_APPLY_MODE=manual
# ── Public landing page (OFF by default; full + per-field tunable when on) ──
PUBLIC_LANDING_ENABLED=false
@@ -234,9 +312,15 @@ PUBLIC_STATS_ENABLED=false
PUBLIC_STATS_CACHE_HOURS=24
# ── Data retention / erasure ──────────────────────────────────────────────
# When ERASURE_ENABLED=true, self-service erasure needs ADMIN_KEY_INTEGRITY_SECRET
# (its audit trail is the integrity-MAC'd hash chain) and PUBLIC_API_URL (its
# proofs bind that URL); boot fails closed otherwise.
ERASURE_ENABLED=false
ERASURE_PURGE_LOGIN_EVENTS=true
ERASURE_EXPORT_PER_DAY=3
# Grace window (hours) between a confirmed erasure and its execution. Max 87600
# (10 years).
ERASURE_GRACE_PERIOD_HOURS=72
# Log retention (auto-deletion); 0 = keep indefinitely.
RETENTION_LOGIN_EVENTS_DAYS=90
RETENTION_AUDIT_DAYS=365