mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
Falling back to public Electrum is a PRIVACY decision, not just an availability one: the queries carry user addresses. It is easy to miss because the failover is silent and correct, so the only symptom is that balances keep working. Our own LTC Fulcrum was dead for 11 days with every lookup going to third parties, and nothing surfaced it. - startup: log how many third-party fallbacks are configured and how to fail closed instead, so the posture is visible in the operator's own logs - .env.example: state the tradeoff at the setting, with both choices spelled out rather than a default presented as obviously correct - OPERATOR_SETUP: same note where an operator configures chains Also expands the shipped fallback lists (BTC 3 -> 8, LTC 4 -> 7), which is both more resilience and a wider spread of which server sees any given query.
366 lines
20 KiB
Bash
366 lines
20 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
|
|
# 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
|
|
|
|
# ── 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
|
|
# 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
|
|
# `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
|
|
|
|
# ── 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
|
|
|
|
# ── Address-privacy features (opt-in) ─────────────────────────────────────
|
|
# Per-payment receive addresses. Off by default; enable once your node stack
|
|
# meets the requirement noted below.
|
|
#
|
|
# FEATURE_XMR_SUBADDR_PROVISIONING: provisions account-0 subaddress indices on
|
|
# the LWS at register/import so per-payment XMR/WOW receive addresses are
|
|
# scanned. REQUIRES your monero-lws / wownero-lws to run with
|
|
# `--max-subaddresses >= 200` (it defaults to 0, which disables subaddress
|
|
# support entirely). Confirm with:
|
|
# curl -s http://<lws>/get_version | grep -o '"max_subaddresses":[0-9]*'
|
|
# The backend probes this at provision time and refuses with a clear error
|
|
# rather than half-working, so a wrong setting is loud, not silent.
|
|
FEATURE_XMR_SUBADDR_PROVISIONING=false
|
|
#
|
|
# FEATURE_UTXO_MULTI_ADDRESS: mounts the batched BTC/LTC endpoints
|
|
# (/wallet/utxo/{balance,utxos,history}_multi) used by HD gap-limit wallets,
|
|
# capped at 32 addresses per request. When off, the routes are not mounted and
|
|
# the single-address endpoints are unchanged.
|
|
FEATURE_UTXO_MULTI_ADDRESS=false
|
|
|
|
# ── Bitcoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ─────
|
|
BTC_NETWORK=mainnet
|
|
BTC_ELECTRUM_URL=
|
|
#
|
|
# ── PRIVACY: read this before setting fallbacks ───────────────────────────────
|
|
# These are THIRD-PARTY servers. When your own Electrum/Fulcrum is unreachable,
|
|
# the backend fails over to them, and the queries it sends carry YOUR USERS'
|
|
# ADDRESSES. The failover is silent and correct, so the only symptom is that
|
|
# balances keep working: a dead Fulcrum can go unnoticed for days while every
|
|
# lookup goes to someone else's server. (That is not hypothetical; it happened
|
|
# to us for 11 days.)
|
|
#
|
|
# Two honest choices, both defensible:
|
|
# * LEAVE THESE SET for availability. Balances survive your node dying. Watch
|
|
# for the "third-party Electrum fallback(s) configured" line at startup.
|
|
# * LEAVE THEM EMPTY to fail closed. Balances break when your node breaks, and
|
|
# every query stays on infrastructure you control. This is the right default
|
|
# for a privacy-first or air-gapped deployment.
|
|
#
|
|
# More entries = more resilience and a wider spread of who sees a given query.
|
|
# They are tried in order after the primary.
|
|
BTC_ELECTRUM_FALLBACKS=ssl://electrum.blockstream.info:50002,ssl://electrum.emzy.de:50002,ssl://electrum.bitaroo.net:50002,ssl://fulcrum.sethforprivacy.com:50002,ssl://electrum.diynodes.com:50002,ssl://bitcoin.lu.ke:50002,ssl://electrum.jochen-hoenicke.de:50006,ssl://btc.electroncash.dk:60002
|
|
# 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
|
|
LTC_ELECTRUM_URL=
|
|
# Same privacy tradeoff as BTC_ELECTRUM_FALLBACKS above; read that note.
|
|
LTC_ELECTRUM_FALLBACKS=ssl://electrum-ltc.bysh.me:50002,ssl://electrum.ltc.xurious.com:50002,ssl://backup.electrum-ltc.org:443,ssl://electrum1.cipig.net:20063,ssl://electrum2.cipig.net:20063,ssl://electrum3.cipig.net:20063,ssl://ltc.rentonisk.com:50002
|
|
|
|
# ── 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
|
|
# 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
|
|
# 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/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
|
|
# 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
|
|
# 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
|
|
# 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 ──────────────────────────────────────────────
|
|
# 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
|