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. # so every real deployment must replace it.
PUBLIC_API_URL=https://backend.example.org/api/v1 PUBLIC_API_URL=https://backend.example.org/api/v1
ENVIRONMENT=development 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 ──────────────────────────────────────────────────────────────
DATABASE_URL=postgres://user:password@127.0.0.1:5432/smirk DATABASE_URL=postgres://user:password@127.0.0.1:5432/smirk
@@ -36,6 +49,10 @@ FEATURE_POW=false
ALTCHA_HMAC_KEY= ALTCHA_HMAC_KEY=
POW_REQUIRED=false POW_REQUIRED=false
ALTCHA_COST=100000 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) ──── # ── Registration gates (composable; each advertised via /capabilities) ────
# Require an operator-minted invite code to create a NEW wallet. Mint codes with # 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. # enabled the two modes are identical.
REGISTRATION_GATE_MODE=all 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) ───── # ── Bitcoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ─────
BTC_NETWORK=mainnet BTC_NETWORK=mainnet
BTC_ELECTRUM_URL= BTC_ELECTRUM_URL=
BTC_ELECTRUM_FALLBACKS= 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) ──── # ── Litecoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ────
LTC_NETWORK=mainnet LTC_NETWORK=mainnet
@@ -142,8 +181,26 @@ WALLET_RESTORE_POW_MAX_BITS=24
FEATURE_PRICES=true FEATURE_PRICES=true
PRICES_PROVIDER=coingecko PRICES_PROVIDER=coingecko
PRICES_FETCH_INTERVAL_SECS=300 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. # Tips are parked for the conditional-transfer redesign; off until that lands.
FEATURE_TIPS=false 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) ──── # ── Nostr relay (optional messaging plane; see docs/operations/RELAY.md) ────
# Off by default. When on, the operator runs a relay (the packaged # Off by default. When on, the operator runs a relay (the packaged
@@ -205,11 +262,21 @@ FEED_OWNER_NPUB=
FEED_ALLOWLIST_NPUBS= FEED_ALLOWLIST_NPUBS=
FEED_EXTRA_RELAYS= 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, # 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. # 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_ENABLED=false
ADMIN_BIND=127.0.0.1:8081 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 # 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 # 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 # 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_KEY_INTEGRITY_SECRET=
ADMIN_MAX_KEYS=8 ADMIN_MAX_KEYS=8
ADMIN_PENDING_KEY_TTL_DAYS=7 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 page (OFF by default; full + per-field tunable when on) ──
PUBLIC_LANDING_ENABLED=false PUBLIC_LANDING_ENABLED=false
@@ -234,9 +312,15 @@ PUBLIC_STATS_ENABLED=false
PUBLIC_STATS_CACHE_HOURS=24 PUBLIC_STATS_CACHE_HOURS=24
# ── Data retention / erasure ────────────────────────────────────────────── # ── 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_ENABLED=false
ERASURE_PURGE_LOGIN_EVENTS=true ERASURE_PURGE_LOGIN_EVENTS=true
ERASURE_EXPORT_PER_DAY=3 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. # Log retention (auto-deletion); 0 = keep indefinitely.
RETENTION_LOGIN_EVENTS_DAYS=90 RETENTION_LOGIN_EVENTS_DAYS=90
RETENTION_AUDIT_DAYS=365 RETENTION_AUDIT_DAYS=365
+4
View File
@@ -72,6 +72,10 @@ Operator/admin functions live on a **separate loopback listener** (front it with
Tor or an SSH tunnel) — confidentiality by socket, never merged into the public Tor or an SSH tunnel) — confidentiality by socket, never merged into the public
router or OpenAPI. router or OpenAPI.
- **Operator Console**: an embedded single-page app served at `/admin` on the
loopback admin plane. Sign in with your admin key, then review status, manage
admin keys and invite codes, and edit the runtime config overlay (env plus a
validated DB layer). Guide: [docs/operations/CONSOLE.md](docs/operations/CONSOLE.md).
- **Sign-in-with-Smirk admin auth** — a NIP-98 *signed action* over a single-use - **Sign-in-with-Smirk admin auth** — a NIP-98 *signed action* over a single-use
nonce (AUTHN) + a MAC-protected key allowlist (AUTHZ), composed so a route can't nonce (AUTHN) + a MAC-protected key allowlist (AUTHZ), composed so a route can't
run without both. run without both.
+219
View File
@@ -0,0 +1,219 @@
# Operator Console: admin surface guide
The Operator Console is an embedded single-page app served at `/admin` on the
backend's loopback admin plane. It is the browser front end for the same admin
JSON API the `smirk-admin` CLI complements: sign in with your admin key, review
status, manage admin keys and invite codes, and edit the runtime config overlay.
The console ships inside the binary (built from `admin-ui/` and embedded at
compile time), so self-hosting needs no separate front-end deploy. It is off
unless the admin plane is enabled.
## 1. Enable and reach the admin plane
The admin plane is a separate listener from the public API; confidentiality is by
socket, not by middleware ordering. It never joins the public router or the
OpenAPI surface.
Enable it in your `.env`:
```sh
ADMIN_ENABLED=true
ADMIN_BIND=127.0.0.1:8081 # loopback by default
ADMIN_PUBLIC_URL=https://admin.example.org
ADMIN_JWT_SECRET=<openssl rand -hex 32>
ADMIN_KEY_INTEGRITY_SECRET=<openssl rand -hex 32> # back this up; losing it locks out admin
```
`ADMIN_BIND` defaults to loopback. Boot refuses a non-loopback bind unless you
also set `ADMIN_ALLOW_PUBLIC_BIND=true`, so the plane cannot be exposed on a
public interface by accident. Never bind it to a public address; reach it one of
two ways instead:
- **SSH tunnel.** `ssh -L 8081:127.0.0.1:8081 you@your-server`, then open
`http://127.0.0.1:8081/admin` in your local browser. Because the Host is
`127.0.0.1`, the admin plane's Host allowlist accepts it with no extra config.
- **Tor onion service.** Point a hidden service at `127.0.0.1:8081` and set
`TOR_ADMIN_ONION=<hash>.onion`. The onion host is added to the admin Host
allowlist; without `TOR_ADMIN_ONION`, a request whose `Host` header is the
onion is rejected with `403` (the allowlist defends the loopback socket against
DNS rebinding, and only loopback names plus the one configured onion pass).
Then browse `http://<hash>.onion/admin` in Tor Browser.
The console shell (`GET /admin`, `/admin/`, `/admin/assets/*`) is served
unauthenticated: it loads without a session and runs the login client-side. Every
JSON route it calls (`/admin/keys`, `/admin/config`, `/admin/invites`, ...) is
guarded, so serving the shell openly exposes nothing.
### `ADMIN_PUBLIC_URL` is the per-node identity
`ADMIN_PUBLIC_URL` is the value your wallet's signed action binds against (the
NIP-98 `u` tag), and a per-node id is derived from it so a challenge signed for
one node cannot be replayed to another node in a fleet. Give each node in a fleet
a distinct `ADMIN_PUBLIC_URL`. The server dictates and verifies this value from
config, never from the request `Host` header, so the value you reach the plane at
(loopback, tunnel, onion) does not have to equal `ADMIN_PUBLIC_URL`.
## 2. Sign in with your admin key
Login is a NIP-98 signed action ("Sign in with Smirk"): you prove control of your
admin private key by signing a single-use, server-issued challenge with your
Smirk wallet. Only admin public keys are ever stored; the server never holds an
admin seed or private key.
1. Bootstrap a first admin key if you have not already (see
[OPERATOR_SETUP.md](OPERATOR_SETUP.md) section 5): `smirk-admin setup --pubkey
<x-only-hex>` seeds your pubkey active, or `smirk-admin create-admin-wallet`
generates a pending key that activates on its first login.
2. Open the console and start login. It calls `POST /admin/auth/challenge`, which
returns a nonce, the `u` URL to bind, and the instance id.
3. Sign the kind-27235 (NIP-98) challenge event with the Smirk wallet holding your
admin key. The console posts it to `POST /admin/auth/verify`.
4. On success you receive an admin access token plus a refresh token, and the
console loads the authenticated tabs. A pending key activates on this first
login.
Every admin-auth rejection (bad signature, a valid signature for a non-admin key,
a missing session) collapses to the same opaque `401`, so the surface is not an
enumeration oracle. Login and privileged actions are written to a hash-chained,
tamper-evident audit trail.
## 3. Console tabs
- **Status**: the effective feature/capability view, including any downgrades
(a flag that is on in config but serving as disabled because its secret or URL
is unset, e.g. `xmr` on with `XMR_LWS_ADMIN_KEY` empty).
- **Keys**: add, list, revoke, and rotate admin keys. A new key is added pending
and activates on its holder's first login. Revoking the last live key over the
network is refused (use the CLI); rotate is allowed on a solo key because it is
add-plus-revoke, the in-band recovery for a compromised sole key.
- **Invites**: mint single-use invite codes (`POST /admin/invites`) and list
them (`GET /admin/invites`). Raw codes are shown once; only their hash is
stored.
- **Config**: the runtime config overlay editor (below).
## 4. Config: the env-vs-DB precedence model
Effective config is layered:
```
defaults -> environment (.env) -> DB overlay (operator settings) -> validate
```
The DB overlay is a sparse, per-section patch stored in the `operator_settings`
table, one MAC'd row per section. A field present in the overlay overrides env for
that field; an absent field keeps its env (or default) value. `GET /admin/config`
returns, for every editable field, its effective value, the DB overlay, the
per-section version, its runtime class, and whether a restart is pending. So the
console can show each field's current value and whether its source is env/default
or the DB overlay.
Editing goes through `PUT /admin/config` with a sparse patch. The patch is merged
onto the persisted overlay and re-run through the exact same `Config::validate`
that boot uses, so a bad edit is rejected with the identical error boot would
raise and nothing invalid is ever persisted or applied. Secrets are excluded from
the editable surface by construction: no overlay field maps to a secret (JWT
secrets, peppers, the ALTCHA HMAC key, the payment processor API key, the relay
admission bind), so the console can neither read nor set one. Secrets stay in env.
The nine editable sections are: `landing`, `retention`, `restore`, `console`,
`registration`, `pow`, `features`, `relay`, `premium`.
### Runtime-safe vs restart-required fields
Each field is classified as runtime-safe (the change hot-swaps onto the live
config and takes effect on the next request) or restart-required (the change is
persisted and applied on the next graceful restart, because a boot-built client,
socket, or worker captured the old value). The classification is the single
source of truth in `runtime_class()` and is returned per field by
`GET /admin/config`. The exact classification:
| Section | Class | Notes |
| --- | --- | --- |
| `landing.*` | runtime-safe | read per request on the landing/render path |
| `retention.*` | runtime-safe | **except** `retention.erasure_enabled` |
| `retention.erasure_enabled` | restart-required | gates a boot-spawned erasure sweep worker |
| `restore.*` | runtime-safe | restore policy is read per request |
| `console.*` | runtime-safe | `restart_apply_mode` is read at save time |
| `registration.*` | runtime-safe | **except** `registration.require_payment` |
| `registration.require_payment` | restart-required | gates the boot-built payment client (the provider is `None` at boot unless payment or premium is on) |
| `pow.*` | runtime-safe | the PoW gate is read per request |
| `features.*` | restart-required | every field: feature flags and per-chain enablement gate boot-built clients/workers (price poller, chain adapters) |
| `relay.*` | restart-required | the relay/admission service is boot-bound and the URL is advertised at boot |
| `premium.*` | restart-required | premium is enforced by the boot-bound relay admission service |
Any `(section, field)` not listed defaults to restart-required, the safe default:
never hot-swap something whose consumers have not been reasoned about.
A `PUT` that touches only runtime-safe fields hot-swaps just that patch onto the
running config and reports `applied: "runtime"`. A `PUT` that touches any
restart-required field persists the change and reports `applied:
"restart-required"` with `restart_pending: true`. `restart_pending` stays true
until a restart applies it, and it reflects any restart-required change saved by
an earlier `PUT`, even when the current edit was itself live.
### Optimistic concurrency (`expected_versions`)
Each section carries a version that increments on every write. To guard against
two operators editing at once, send `expected_versions` in the `PUT` body with the
version you last read per touched section. If the stored version has moved on, the
write is rejected with `409 Conflict` and nothing is persisted; reload and retry.
Omitting `expected_versions` skips the check (last write wins).
### `restart_apply_mode`: manual vs auto
`console.restart_apply_mode` (env `RESTART_APPLY_MODE`, default `manual`) decides
how a restart-required change is applied:
- `manual` (default): the change is persisted and flagged `restart_pending`. You
apply it yourself with a graceful restart of the service.
- `auto`: after saving a restart-required change, the instance self-restarts
shortly after responding so the change takes effect. The console warns before
saving in this mode.
Auto mode REQUIRES the service to be brought back on exit. The self-restart is a
graceful shutdown (it drains in-flight requests and exits); nothing relaunches the
process on its own. Run under systemd with `Restart=always` (or an equivalent
supervisor) so the instance comes back with the new config applied. Without that,
`auto` takes the instance down and leaves it down. Do not use `auto` for a
process you start by hand.
### Clearing a DB override to fall back to env
The overlay is stored per section, and a value you set through the console is a DB
override that wins over env for that field. Note the behavior in this version:
- There is no per-field null clear. A `null` in a `PUT` patch means "leave this
field unchanged" and is skipped, so sending `null` does not remove an existing
override.
- To make a field's effect match env again, set it back through the console to the
same value env provides. The effective value then equals env, though the field's
source still reports `db`.
- `landing.title` is the one special case: setting it to an empty string clears it
back to the env value (unset).
- To fully drop a section's DB overlay so its source returns to env/default (and
future env edits for that section flow through again), delete that section's row
from the `operator_settings` table and restart, for example:
```sql
DELETE FROM operator_settings WHERE section = 'landing';
```
On load, any section whose row is absent, whose MAC fails, or which fails to
decode is ignored and falls back to env, fail-closed on tamper and
fail-open-to-env on a missing section. There is no console/API/CLI command to
clear a section in this version; the direct table delete is the supported path.
## 5. Reserved / inert config
- `ADMIN_PUBKEYS` is parsed from the environment but is NOT consumed anywhere in
this version: it does not gate admin access. Admin authorization is the
MAC-protected key allowlist in the database (managed via `smirk-admin` and the
Keys tab), never this variable. Treat `ADMIN_PUBKEYS` as reserved and leave it
unset; setting it has no effect and does not grant access.
## Related
- [OPERATOR_SETUP.md](OPERATOR_SETUP.md): full deployment and admin bootstrap.
- [RELAY.md](RELAY.md): the optional Nostr relay messaging plane.
- The root [`.env.example`](../../.env.example) documents every environment key.
+13
View File
@@ -100,6 +100,19 @@ Expose the admin plane over a Tor onion or an SSH tunnel; never bind it to a
public interface. `ADMIN_BIND` defaults to loopback, and a non-loopback bind public interface. `ADMIN_BIND` defaults to loopback, and a non-loopback bind
requires an explicit `ADMIN_ALLOW_PUBLIC_BIND=true`. requires an explicit `ADMIN_ALLOW_PUBLIC_BIND=true`.
- **SSH tunnel:** `ssh -L 8081:127.0.0.1:8081 you@server`, then reach the plane at
`http://127.0.0.1:8081`. The loopback Host passes the admin allowlist with no
extra config.
- **Tor onion:** point a hidden service at `127.0.0.1:8081` and set
`TOR_ADMIN_ONION=<hash>.onion`. That onion host is added to the admin Host
allowlist; without it, a request whose `Host` header is the onion is rejected
(`403`), because the allowlist only accepts loopback names plus the one
configured onion (anti DNS-rebinding).
The Operator Console (an embedded admin SPA served at `/admin` on this same
plane) gives you a browser front end for keys, invites, status, and the runtime
config overlay. See [CONSOLE.md](CONSOLE.md).
## 6. Registration policy (optional) ## 6. Registration policy (optional)
Gates are composable and advertised via `/capabilities`; returning wallets and Gates are composable and advertised via `/capabilities`; returning wallets and
+18
View File
@@ -37,11 +37,29 @@ recipient), which a static pubkey whitelist can't do.
| `inbox-outbox` (default) | registered npubs publish their own events (outbox); **anyone** may deliver a NIP-17 gift-wrap (kind 1059) **addressed to a registered user** (inbox) | | `inbox-outbox` (default) | registered npubs publish their own events (outbox); **anyone** may deliver a NIP-17 gift-wrap (kind 1059) **addressed to a registered user** (inbox) |
| `author-allowlist` | only registered Smirk npubs may publish anything (no external inbound) | | `author-allowlist` | only registered Smirk npubs may publish anything (no external inbound) |
| `open` | accept everything (the relay's resource caps still apply) | | `open` | accept everything (the relay's resource caps still apply) |
| `premium-post` | registered users post wallet-functional events (DMs, tips, swap coordination) free; **general** notes require an active premium subscription (see the premium tier below). This is the flagship monetized relay policy |
`RELAY_INBOUND_POW_BITS` (0 = off) requires a NIP-13 proof-of-work on `RELAY_INBOUND_POW_BITS` (0 = off) requires a NIP-13 proof-of-work on
cross-ecosystem (non-registered-author) inbound events — spam friction without an cross-ecosystem (non-registered-author) inbound events — spam friction without an
author allowlist. Only the `inbox-outbox` external-inbound branch is gated. author allowlist. Only the `inbox-outbox` external-inbound branch is gated.
`RELAY_WRITE_ALLOWLIST_NPUBS` (comma-separated `npub1…` or 64-char hex; empty by
default) lists write-exempt npubs that may publish **any** kind regardless of the
write policy or premium status. The intended use is an announcements or
feed-owner account that seeds a `premium-post` feed without itself holding a
subscription. It is an exemption list, not the `author-allowlist` policy.
## Premium tier (`premium-post`)
The `premium-post` policy pairs with the premium subscription block
(`PREMIUM_ENABLED=true`, `PREMIUM_CURRENCY`, `PREMIUM_PLANS`) and the `PAYMENT_*`
processor. Wallet use stays free; premium unlocks general Nostr posting to your
relay. `Config::validate` fails closed if `PREMIUM_ENABLED` is set without
`RELAY_ENABLED=true`, `RELAY_WRITE_POLICY=premium-post`, the processor
credentials, and at least one priced `PREMIUM_PLANS` entry. Full design:
[../design/premium-relay.md](../design/premium-relay.md); ready-to-copy config:
the [`paid-relay`](../../examples/paid-relay/) recipe.
## Enable it ## Enable it
1. Set the relay env (see `.env.example` → *Nostr relay*): 1. Set the relay env (see `.env.example` → *Nostr relay*):
+13
View File
@@ -43,6 +43,19 @@ RELAY_WRITE_POLICY=author-allowlist
RELAY_INBOUND_POW_BITS=0 RELAY_INBOUND_POW_BITS=0
RELAY_ADMISSION_BIND=127.0.0.1:8090 RELAY_ADMISSION_BIND=127.0.0.1:8090
# ── Chains: enable ONLY what this recipe wires up ────────────────────────────
# FAIL-CLOSED RULE: in production, an ENABLED chain with no source configured makes
# the server REFUSE TO BOOT. Every FEATURE_* chain defaults to true, so this recipe
# turns off the chains it does not wire up. It wires up Monero + Wownero (the
# XMR_/WOW_ blocks below); fill their *_LWS_ADMIN_KEY and it boots. To add BTC/LTC
# (electrum URL) or GRIN (GRIN_OWNER_API_SECRET + wiring), configure the source
# first, THEN flip its flag to true.
FEATURE_XMR=true
FEATURE_WOW=true
FEATURE_BTC=false
FEATURE_LTC=false
FEATURE_GRIN=false
# Chain endpoints — see ../../.env.example. # Chain endpoints — see ../../.env.example.
XMR_LWS_URL= XMR_LWS_URL=
XMR_LWS_ADMIN_URL= XMR_LWS_ADMIN_URL=
+13
View File
@@ -45,6 +45,19 @@ RELAY_WRITE_POLICY=inbox-outbox
RELAY_INBOUND_POW_BITS=0 RELAY_INBOUND_POW_BITS=0
RELAY_ADMISSION_BIND=127.0.0.1:8090 RELAY_ADMISSION_BIND=127.0.0.1:8090
# ── Chains: enable ONLY what this recipe wires up ────────────────────────────
# FAIL-CLOSED RULE: in production, an ENABLED chain with no source configured makes
# the server REFUSE TO BOOT. Every FEATURE_* chain defaults to true, so this recipe
# turns off the chains it does not wire up. It wires up Monero + Wownero (the
# XMR_/WOW_ blocks below); fill their *_LWS_ADMIN_KEY and it boots. To add BTC/LTC
# (electrum URL) or GRIN (GRIN_OWNER_API_SECRET + wiring), configure the source
# first, THEN flip its flag to true.
FEATURE_XMR=true
FEATURE_WOW=true
FEATURE_BTC=false
FEATURE_LTC=false
FEATURE_GRIN=false
# Chain endpoints — point at your own nodes / LWS. See ../../.env.example. # Chain endpoints — point at your own nodes / LWS. See ../../.env.example.
XMR_LWS_URL= XMR_LWS_URL=
XMR_LWS_ADMIN_URL= XMR_LWS_ADMIN_URL=
+13
View File
@@ -50,6 +50,19 @@ FEATURE_PRICES=true
FEATURE_TIPS=false FEATURE_TIPS=false
RELAY_ENABLED=false RELAY_ENABLED=false
# ── Chains: enable ONLY what this recipe wires up ────────────────────────────
# FAIL-CLOSED RULE: in production, an ENABLED chain with no source configured makes
# the server REFUSE TO BOOT. Every FEATURE_* chain defaults to true, so this recipe
# turns off the chains it does not wire up. It wires up Monero + Wownero (the
# XMR_/WOW_ blocks below); fill their *_LWS_ADMIN_KEY and it boots. To add BTC/LTC
# (electrum URL) or GRIN (GRIN_OWNER_API_SECRET + wiring), configure the source
# first, THEN flip its flag to true.
FEATURE_XMR=true
FEATURE_WOW=true
FEATURE_BTC=false
FEATURE_LTC=false
FEATURE_GRIN=false
# Chain endpoints — see ../../.env.example. # Chain endpoints — see ../../.env.example.
XMR_LWS_URL= XMR_LWS_URL=
XMR_LWS_ADMIN_URL= XMR_LWS_ADMIN_URL=
+13
View File
@@ -71,6 +71,19 @@ FEATURE_PRICES=true
FEATURE_TIPS=false FEATURE_TIPS=false
ADMIN_ENABLED=false ADMIN_ENABLED=false
# ── Chains: enable ONLY what this recipe wires up ────────────────────────────
# FAIL-CLOSED RULE: in production, an ENABLED chain with no source configured makes
# the server REFUSE TO BOOT. Every FEATURE_* chain defaults to true, so this recipe
# turns off the chains it does not wire up. It wires up Monero + Wownero (the
# XMR_/WOW_ blocks below); fill their *_LWS_ADMIN_KEY and it boots. To add BTC/LTC
# (electrum URL) or GRIN (GRIN_OWNER_API_SECRET + wiring), configure the source
# first, THEN flip its flag to true.
FEATURE_XMR=true
FEATURE_WOW=true
FEATURE_BTC=false
FEATURE_LTC=false
FEATURE_GRIN=false
# Chain endpoints — see ../../.env.example (XMR/WOW LWS + daemon, GRIN_*, electrum). # Chain endpoints — see ../../.env.example (XMR/WOW LWS + daemon, GRIN_*, electrum).
XMR_LWS_URL= XMR_LWS_URL=
XMR_LWS_ADMIN_URL= XMR_LWS_ADMIN_URL=
+13
View File
@@ -45,6 +45,19 @@ RELAY_ENABLED=false
ADMIN_ENABLED=false ADMIN_ENABLED=false
ADMIN_KEY_INTEGRITY_SECRET= ADMIN_KEY_INTEGRITY_SECRET=
# ── Chains: enable ONLY what this recipe wires up ────────────────────────────
# FAIL-CLOSED RULE: in production, an ENABLED chain with no source configured makes
# the server REFUSE TO BOOT. Every FEATURE_* chain defaults to true, so this recipe
# turns off the chains it does not wire up. It wires up Monero + Wownero (the
# XMR_/WOW_ blocks below); fill their *_LWS_ADMIN_KEY and it boots. Running your own
# node, you will likely want more chains: to add BTC/LTC set the electrum URL, to
# add GRIN set GRIN_OWNER_API_SECRET (+ wiring), THEN flip that chain's flag to true.
FEATURE_XMR=true
FEATURE_WOW=true
FEATURE_BTC=false
FEATURE_LTC=false
FEATURE_GRIN=false
# Chain endpoints — point at YOUR nodes / light-wallet servers. See the full list # Chain endpoints — point at YOUR nodes / light-wallet servers. See the full list
# and formats in ../../.env.example (XMR/WOW LWS + daemon, GRIN_*, BTC/LTC electrum). # and formats in ../../.env.example (XMR/WOW LWS + daemon, GRIN_*, BTC/LTC electrum).
XMR_LWS_URL= XMR_LWS_URL=
+13
View File
@@ -49,6 +49,19 @@ RELAY_ENABLED=false
# Admin HTTP plane off at launch; enable + bootstrap a key when you need it. # Admin HTTP plane off at launch; enable + bootstrap a key when you need it.
ADMIN_ENABLED=false ADMIN_ENABLED=false
# ── Chains: enable ONLY what this recipe wires up ────────────────────────────
# FAIL-CLOSED RULE: in production, an ENABLED chain with no source configured makes
# the server REFUSE TO BOOT. Every FEATURE_* chain defaults to true, so this recipe
# turns off the chains it does not wire up. It wires up Monero + Wownero (the
# XMR_/WOW_ blocks below); fill their *_LWS_ADMIN_KEY and it boots. To add BTC/LTC
# (electrum URL) or GRIN (GRIN_OWNER_API_SECRET + wiring), configure the source
# first, THEN flip its flag to true.
FEATURE_XMR=true
FEATURE_WOW=true
FEATURE_BTC=false
FEATURE_LTC=false
FEATURE_GRIN=false
# Chain endpoints — reuse your existing daemons / LWS. See ../../.env.example. # Chain endpoints — reuse your existing daemons / LWS. See ../../.env.example.
XMR_LWS_URL= XMR_LWS_URL=
XMR_LWS_ADMIN_URL= XMR_LWS_ADMIN_URL=