Files
smirk-backend-core/.env.example
jwinterm 4d1d9006bc feat(grin): proxy scans through grin-lws (default) with grin-wallet fallback
Add a GrinLwsClient (a faithful mirror of the XMR LwsClient) + GrinLwsConfig
(enabled when GRIN_LWS_URL is set). /wallet/grin/scan now defaults to grin-lws
when configured and returns derivation paths (key_id/n_child) + spendability,
letting the wallet skip its client-side output-identify search.

Money-safety: the restore-depth/PoW gate runs once and is terminal (never
masked by fallback); grin-lws is trusted only when provably synced
(scanned_height + margin >= tip), otherwise the scan falls back to the
authoritative grin-wallet scan_rewind_hash; a grin-lws transport error also
falls back; grin-wallet errors propagate as 503. No path ever synthesizes an
empty/zero balance.

Off unless GRIN_LWS_URL is configured (prod grin behavior unchanged until
pointed at a grin-lws instance). Capabilities advertise `grin_lws`.
openapi.json regenerated.
2026-07-09 16:26:23 -04:00

243 lines
12 KiB
Bash

# smirk-backend-core — example environment.
#
# Copy to `.env` and fill in for your deployment. NEVER commit a real `.env`.
# Generate secrets with: openssl rand -hex 32
# ── Server ────────────────────────────────────────────────────────────────
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
# Public absolute API base URL — set this to YOUR real domain. It is the canonical
# value the NIP-98 `u` tag is verified against (never the request Host header). In
# production, boot fails closed if this is left as a placeholder (contains `example`
# or `your-`) while Nostr identity (on by default), admin, or erasure is enabled —
# so every real deployment must replace it.
PUBLIC_API_URL=https://backend.example.org/api/v1
ENVIRONMENT=development
# ── Database ──────────────────────────────────────────────────────────────
DATABASE_URL=postgres://user:password@127.0.0.1:5432/smirk
# ── Auth (JWT) ────────────────────────────────────────────────────────────
JWT_SECRET=
JWT_EXPIRY_HOURS=24
# ── At-rest identity secrets (REQUIRED; generate each: openssl rand -hex 32) ─
# HMAC peppers keep stored identity values (pubkey_hash, seed_fingerprint)
# non-reproducible from a candidate seed; the salt unlinks client IPs in abuse
# tables. Boot fails closed if any is unset or too short.
SEED_FINGERPRINT_PEPPER=
REFRESH_TOKEN_PEPPER=
IP_SALT=
# ── Proof-of-work signup gate (ALTCHA v2) ─────────────────────────────────
# FEATURE_POW is the master switch; POW_REQUIRED only applies when it is on. Boot
# fails closed if POW_REQUIRED=true while FEATURE_POW=false (a silently-open gate).
FEATURE_POW=false
ALTCHA_HMAC_KEY=
POW_REQUIRED=false
ALTCHA_COST=100000
# ── Registration gates (composable; each advertised via /capabilities) ────
# Require an operator-minted invite code to create a NEW wallet. Mint codes with
# `smirk-admin mint-invite --count <n>`. Returning wallets bypass it, and
# self-hosting bypasses all gates (run your own backend). PoW is the ALTCHA gate
# above. Compose freely (e.g. invite + PoW + pay-to-register).
REGISTRATION_REQUIRE_INVITE=false
# Pay-to-register: require a SETTLED payment invoice to create a new wallet. The
# invoice is minted at POST /auth/payment-invoice and paid — directly to YOUR own
# wallet — via an external, non-custodial processor. The `btcpay` adapter speaks
# the BTCPay-compatible invoice API, which also covers this project's xmrcheckout
# /wowcheckout apps and the BTCPay Monero plugin, so you pick the coin by pointing
# at the processor that accepts it. The backend only READS invoice status (pull
# model) — it never holds funds or keys. Self-hosting bypasses this (leave off).
REGISTRATION_REQUIRE_PAYMENT=false
PAYMENT_PROVIDER=btcpay
# Processor base URL, store/merchant id, and an API key that can create + read
# invoices (held redacted; never logged). Required when the gate is on.
PAYMENT_PROVIDER_URL=https://pay.example.org
PAYMENT_STORE_ID=
PAYMENT_API_KEY=
# Registration price + currency ('XMR', or a fiat code the processor converts).
PAYMENT_AMOUNT=0.01
PAYMENT_CURRENCY=XMR
# Confirmations-to-finalize (mapped to the processor's speed policy), and how long
# an unpaid invoice stays payable before it expires.
PAYMENT_CONFIRMATIONS=1
PAYMENT_EXPIRES_MINUTES=60
# How the enabled invite/payment gates COMBINE (PoW is orthogonal and always
# applies on top). 'all' (default): a new wallet must satisfy EVERY enabled gate
# (invite AND payment when both are on). 'any': the enabled gates are
# ALTERNATIVES and the wallet satisfies exactly ONE (e.g. "an invite code OR pay
# to register"); the client shows a "pick a method" chooser. With a single gate
# enabled the two modes are identical.
REGISTRATION_GATE_MODE=all
# ── Bitcoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ─────
BTC_NETWORK=mainnet
BTC_ELECTRUM_URL=
BTC_ELECTRUM_FALLBACKS=
# ── Litecoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ────
LTC_NETWORK=mainnet
LTC_ELECTRUM_URL=
LTC_ELECTRUM_FALLBACKS=
# ── Monero (daemon + light wallet server) ─────────────────────────────────
XMR_LWS_URL=http://127.0.0.1:8443
XMR_LWS_ADMIN_URL=http://127.0.0.1:9443
XMR_LWS_ADMIN_KEY=
XMR_DAEMON_URL=http://127.0.0.1:18081
# ── Wownero (daemon + light wallet server) ────────────────────────────────
WOW_LWS_URL=http://127.0.0.1:18443
WOW_LWS_ADMIN_URL=http://127.0.0.1:19443
WOW_LWS_ADMIN_KEY=
WOW_DAEMON_URL=http://127.0.0.1:34568
# ── Grin (wallet owner/foreign API + node API) ────────────────────────────
GRIN_OWNER_API_URL=http://127.0.0.1:3420/v3/owner
GRIN_OWNER_API_SECRET=
GRIN_WALLET_PASSWORD=
GRIN_FOREIGN_API_URL=http://127.0.0.1:3415/v2/foreign
GRIN_NODE_API_URL=http://127.0.0.1:3413/v2/owner
GRIN_NODE_API_USER=grin
GRIN_NODE_API_PASS=
GRIN_NODE_FOREIGN_API_URL=http://127.0.0.1:3413/v2/foreign
GRIN_NODE_FOREIGN_API_SECRET=
# ── Grin light-wallet-server (grin-lws) — optional add-on to the Grin chain ─
# When GRIN_LWS_URL is set (and FEATURE_GRIN is on) the Grin scan proxies to
# grin-lws first, trusting it only when it is provably synced to the tip, and
# otherwise falling back to the authoritative grin-wallet scan. Leave GRIN_LWS_URL
# empty to use grin-wallet only. GRIN_LWS_ADMIN_URL / GRIN_LWS_ADMIN_KEY reach
# grin-lws's bearer-gated admin API (optional).
GRIN_LWS_URL=
GRIN_LWS_ADMIN_URL=
GRIN_LWS_ADMIN_KEY=
# ── Wallet restore (import) policy ────────────────────────────────────────
# How far back a wallet may ask THIS instance to scan on import. The backfill
# cost lands on your LWS/node, so it is your call; it is advertised via
# /capabilities so the wallet adapts its import UX. Self-sovereignty is
# preserved — a user needing a deeper restore runs their own instance or picks
# an operator with a looser policy.
# create-only — new wallets only (scan starts at the tip). DEFAULT — and it
# betrays nothing: self-sovereignty is run-your-own-backend.
# bounded — restore up to WALLET_MAX_RESTORE_DEPTH_DAYS behind the tip
# unlimited — any restore height (the natural choice for self-hosting)
WALLET_RESTORE_POLICY=create-only
WALLET_MAX_RESTORE_DEPTH_DAYS=365
# PoW-priced restore: charge the importer compute proportional to the scan depth
# they impose on you. Free below WALLET_RESTORE_POW_FREE_DAYS, then +1 hashcash
# difficulty bit per WALLET_RESTORE_POW_DAYS_PER_BIT days of depth (0 = pricing
# OFF), capped at WALLET_RESTORE_POW_MAX_BITS. Applies within bounded/unlimited.
WALLET_RESTORE_POW_FREE_DAYS=90
WALLET_RESTORE_POW_DAYS_PER_BIT=0
WALLET_RESTORE_POW_MAX_BITS=24
# ── Feature flags (each feature ships in the open core, toggled per deployment) ──
# The wallet client reads GET /api/v1/capabilities and greys out disabled features.
FEATURE_PRICES=true
PRICES_PROVIDER=coingecko
PRICES_FETCH_INTERVAL_SECS=300
# Tips are parked for the conditional-transfer redesign; off until that lands.
FEATURE_TIPS=false
# ── Nostr relay (optional messaging plane; see docs/operations/RELAY.md) ────
# Off by default. When on, the operator runs a relay (the packaged
# nostr-rs-relay under deploy/relay, or an external one) that this backend
# advertises (/capabilities + NIP-05) and write-restricts via a gRPC admission
# service. It is the user's DM INBOX, never the only relay — clients also use the
# public interop relays. Self-hosting concern.
RELAY_ENABLED=false
# bundled (run deploy/relay's nostr-rs-relay) | external (point at your own).
RELAY_MODE=bundled
# Public ws(s):// URL clients connect to (wss required in production).
RELAY_URL=
# Write policy the admission service enforces:
# inbox-outbox — registered npubs publish their own events; anyone may
# deliver a gift-wrapped DM (kind 1059) to a registered user.
# author-allowlist — only registered npubs may publish anything.
# open — accept everything (relay resource caps still apply).
# premium-post — registered users post wallet events (DMs) free; general
# notes require an active PREMIUM_* subscription.
RELAY_WRITE_POLICY=inbox-outbox
# NIP-13 proof-of-work bits required on cross-ecosystem (non-registered-author)
# inbound events; 0 disables the PoW gate.
RELAY_INBOUND_POW_BITS=0
# Write-exempt npubs (comma-separated; npub1… or 64-char hex): may publish ANY
# kind regardless of the write policy or premium status. Use this for an
# announcements / feed-owner account that seeds a premium-post feed without
# holding a subscription. Empty = no exemptions.
RELAY_WRITE_ALLOWLIST_NPUBS=
# Loopback address the gRPC event-admission service binds (the relay calls it).
# It answers "is this npub registered?" (a registration oracle) so it MUST stay
# loopback; the bundled compose reaches it via the host network.
RELAY_ADMISSION_BIND=127.0.0.1:8090
# Allow a NON-loopback admission bind (advanced; e.g. a relay container without
# host networking). Off by default; you must firewall it if you enable it.
RELAY_ADMISSION_ALLOW_PUBLIC=false
RELAY_MAX_EVENT_BYTES=65536
RELAY_RETENTION_DAYS=30
# ── Premium relay tier (optional; recurring, tiered relay-posting access) ──
# Wallet use stays free; premium unlocks GENERAL Nostr posting to the relay
# (wallet-functional events — DMs, tips, swap coordination — stay free). Requires
# RELAY_ENABLED=true + RELAY_WRITE_POLICY=premium-post + the PAYMENT_* processor
# block above. Off by default. See docs/design/premium-relay.md.
PREMIUM_ENABLED=false
# Price currency: a fiat code the processor converts, or a specific coin.
PREMIUM_CURRENCY=USD
# Comma-separated plans, each `id:days:amount` (a built-in tiered discount).
PREMIUM_PLANS=quarter:90:5,halfyear:182:9,year:365:15
# ── Public curated feed (optional; powers a read-only web feed like feed.<domain>) ──
# Reads FROM the relay, so it needs RELAY_ENABLED. All public display config —
# what the feed shows. Off by default; advertised via /capabilities.feed when on.
FEED_ENABLED=false
FEED_SHOW_OWNER=true
FEED_SHOW_PREMIUM=true
# Operator npub (hex or npub1…) for "owner" posts + display; empty if none.
FEED_OWNER_NPUB=
# Comma-separated featured npubs, and extra relays to also pull those authors from.
FEED_ALLOWLIST_NPUBS=
FEED_EXTRA_RELAYS=
# ── Admin surface (see docs; 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.
ADMIN_ENABLED=false
ADMIN_BIND=127.0.0.1:8081
# 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
# distinct one. Must be an absolute URL when ADMIN_ENABLED=true.
ADMIN_PUBLIC_URL=https://admin.example.org
ADMIN_JWT_SECRET=
# Integrity MAC secret protecting admin/setup trust anchors against DB tampering.
# Losing this locks out admin — back it up (see docs/operations).
ADMIN_KEY_INTEGRITY_SECRET=
ADMIN_MAX_KEYS=8
ADMIN_PENDING_KEY_TTL_DAYS=7
# ── Public landing page (OFF by default; full + per-field tunable when on) ──
PUBLIC_LANDING_ENABLED=false
# Optional custom landing title (leave unset for the default).
#PUBLIC_LANDING_TITLE=
PUBLIC_EXPOSE_VERSION=false
PUBLIC_EXPOSE_CHAINS=false
PUBLIC_EXPOSE_PRICE_FEED=false
PUBLIC_EXPOSE_UPTIME=false
PUBLIC_STATS_ENABLED=false
PUBLIC_STATS_CACHE_HOURS=24
# ── Data retention / erasure ──────────────────────────────────────────────
ERASURE_ENABLED=false
ERASURE_PURGE_LOGIN_EVENTS=true
ERASURE_EXPORT_PER_DAY=3
# Log retention (auto-deletion); 0 = keep indefinitely.
RETENTION_LOGIN_EVENTS_DAYS=90
RETENTION_AUDIT_DAYS=365