Files
ukmesh/.env.example
T

293 lines
15 KiB
Bash

# ─────────────────────────────────────────
# MeshCore Analytics — Environment Config
# ─────────────────────────────────────────
# Copy this file to .env and fill in your values.
# Never commit .env to source control.
# Optional host interface for database, MQTT WebSocket, and Redis port
# publishing. The loopback default keeps infrastructure private; set this to a
# trusted LAN interface only when remote access is required and firewalled.
INFRA_BIND_ADDRESS=127.0.0.1
# Required for the backend/operator dashboard. Send as
# `Authorization: Bearer <token>` or `X-Operator-Token`.
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
OPERATOR_SITE_TOKEN=change_me_64_hex_chars
# ── Database ──────────────────────────────────────────────────────────────────
POSTGRES_DB=meshcore
POSTGRES_USER=meshcore
POSTGRES_PASSWORD=change_me_strong_password
# Must exceed Timescale's configured background-worker count (currently 16)
# plus launcher/scheduler and logical-replication headroom. Requires a database
# restart when changed.
POSTGRES_MAX_WORKER_PROCESSES=24
# ── MQTT broker ───────────────────────────────────────────────────────────────
# Internal docker network URL for Mosquitto WebSocket listener
MQTT_BROKER_URL=ws://mosquitto:9001
MQTT_CLIENT_ID=meshcore-analytics-ingest
# Credentials for the backend MQTT client
MQTT_USERNAME=backend
MQTT_PASSWORD=change_me_strong_password
# Bound in-process MQTT work during a burst. The backend uses a persistent
# session and QoS 1 subscriptions so the broker can replay acknowledged QoS 1
# publications after a reconnect; the packet batch writer also retries transient
# database failures.
MQTT_MAX_PAYLOAD_BYTES=65536
MQTT_INGEST_CONCURRENCY=8
MQTT_INGEST_QUEUE_MAX=1000
# Archive and remove repeater visibility after its own observer MQTT feed has
# been silent for one month. Credentials and packet history are preserved.
STALE_MQTT_OBSERVER_CLEANUP_DAYS=30
STALE_MQTT_OBSERVER_CLEANUP_INTERVAL_MS=21600000
# Bound expensive statistics scans so they cannot exhaust the OLTP pool.
DATABASE_POOL_MAX=16
STATS_DB_QUERY_CONCURRENCY=2
# Aggregate reads remain off until the resumable backfill and shadow comparison
# have passed. Never leave shadow mode enabled after rollout validation.
STATS_AGGREGATE_READS_ENABLED=false
STATS_AGGREGATE_SHADOW_ENABLED=false
WARMUP_NETWORKS=ukmesh
PATH_LEARNING_INTERVAL_MS=3600000
# Logical storage budgets used by operator health. The backend measures only
# these mounted data volumes; container root filesystem usage is not reported.
HEALTH_DATABASE_BUDGET_BYTES=322122547200
HEALTH_REDIS_BUDGET_BYTES=4294967296
HEALTH_TERRAIN_BUDGET_BYTES=53687091200
HEALTH_LOG_BUDGET_BYTES=21474836480
# Destructive lifecycle actions are opt-in and additionally require the
# approval/backup/restore gates documented in docs/db-lifecycle.md.
DATA_LIFECYCLE_COMPRESSION_ENABLED=false
DATA_LIFECYCLE_RETENTION_ENABLED=false
DATA_LIFECYCLE_RETENTION_TARGETS=
BACKUP_RECEIPT_HOST_DIR=./backup-receipts
DATA_LIFECYCLE_RECEIPT_MAX_AGE_DAYS=7
# ── Redis ─────────────────────────────────────────────────────────────────────
# Docker Compose protects the internal Redis service with this password. The
# application passes it separately, so REDIS_URL can remain a plain endpoint.
REDIS_PASSWORD=change_me_strong_redis_password
# Optional endpoint override (for an external Redis service). Leave this as the
# internal service URL when using the bundled Compose stack.
REDIS_URL=redis://redis:6379
# ── JWT ───────────────────────────────────────────────────────────────────────
# Generate with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
JWT_SECRET=change_me_64_hex_chars
# ── CORS / allowed origins ────────────────────────────────────────────────────
# Comma-separated list of browser origins allowed to access the API and WebSocket.
# For local development, the defaults in docker-compose.yml cover localhost.
# Set this to your production domains for deployment.
ALLOWED_ORIGINS=https://app.example.com,https://www.example.com
# Per-client public API requests/minute. Keep 120 in production; only isolated
# load-test projects should raise this.
API_RATE_LIMIT_MAX=120
# ── Frontend hostname (build-time) ────────────────────────────────────────────
# If you're running two separate frontends (an analytics app and a public website)
# on different hostnames, set this to the analytics app's hostname.
# When a visitor hits this hostname they see the analytics dashboard;
# all other hostnames serve the public website layout.
# Leave blank to always show the analytics dashboard (simpler single-domain setup).
VITE_APP_HOSTNAME=app.example.com
# Canonical whole-region RF coverage is generated by the pinned HopReach
# source in third_party/hopreach and exposed only on app.ukmesh.com.
RF_COVERAGE_ENABLED=true
HOPREACH_IMAGE=meshcore-analytics-hopreach:local
HOPREACH_CPUS=4.0
HOPREACH_CPU_WORKERS=4
HOPREACH_MEMORY_LIMIT=8g
# ── MeshCore channel secrets ──────────────────────────────────────────────────
# Comma-separated list of AES-128 channel secrets for decrypting GroupText packets.
# Format: name:hex or bare hex (name defaults to first 6 chars of hex).
# The default MeshCore public channel key is always included automatically.
# Example: MESHCORE_CHANNEL_SECRETS=mychannel:9cd4e1f2a3b4c5d6e7f8a9b0c1d2e3f4,aabbccddeeff00112233445566778899
MESHCORE_CHANNEL_SECRETS=
# ── Health check app ──────────────────────────────────────────────────────────
# Optional MeshCore health-check web app for healthcheck.ukmesh.com.
# The app subscribes to observer packet MQTT and waits for users to send a
# generated code to the configured MeshCore channel.
HEALTHCHECK_APP_TITLE=UKMesh Health Check
# Reviewed immutable upstream commit. Branches and tags are rejected at build.
HEALTHCHECK_SOURCE_REF=952eec8e0c55d0749f9abef211c266f992c0bfc7
HEALTHCHECK_TEST_CHANNEL_NAME=ukmeshtest
# Optional source entry name in MESHCORE_CHANNEL_SECRETS when the stored secret
# is named differently from the user-facing channel name.
HEALTHCHECK_TEST_CHANNEL_SECRET_SOURCE_NAME=test
# Optional override. If left blank, the container will reuse the matching
# name:secret entry from MESHCORE_CHANNEL_SECRETS.
HEALTHCHECK_TEST_CHANNEL_SECRET=
# Optional: restrict default scoring to a comma-separated list of observer public keys.
HEALTHCHECK_KNOWN_OBSERVERS=
# Stock defaults are 900 active / 14400 retained. Set both to 3600 if listed
# observers should have sent packets, status, or internal MQTT within one hour.
HEALTHCHECK_OBSERVER_ACTIVE_WINDOW_SECONDS=900
HEALTHCHECK_OBSERVER_RETENTION_SECONDS=14400
# Enable these after adding Cloudflare Turnstile keys.
HEALTHCHECK_TURNSTILE_ENABLED=false
HEALTHCHECK_TURNSTILE_SITE_KEY=
HEALTHCHECK_TURNSTILE_SECRET_KEY=
# ── Owner portal auth ─────────────────────────────────────────────────────────
# Map MQTT usernames to owned node public keys (lowercase 64-hex), comma-separated.
# Format:
# OWNER_MQTT_USERNAME_MAP=username1=nodeid1|nodeid2,username2=nodeid3
# Example:
# OWNER_MQTT_USERNAME_MAP=node1=10cfab...713c95,node-ukmesh=20ed75...fe2ac
OWNER_MQTT_USERNAME_MAP=hermes-test=A2FF345C45EDDCCB453C9F6B1E9973296932CD4C32EC106541D972BFBF4A4FC5
# Start in shadow/shadow, review `owner-auth:inventory`, then switch both to
# enforce/apply only after the inventory has no ambiguous users or missing grants.
OWNER_AUTHORIZATION_MODE=shadow
OWNER_ACL_MODE=shadow
OWNER_ACL_UNMANAGED_USERS=backend,test,test2
# Explicit exception list for intentionally revoked/disabled accounts that
# should retain an empty broker stanza during apply cutover.
OWNER_ACL_ALLOW_EMPTY_USERS=
# Bounded analytics controls. These defaults are intentionally conservative;
# raise them only after reviewing query plans, RSS, and queue latency.
PATH_RESOLVE_WORKERS=2
PATH_RESOLVE_BACKGROUND_QUEUE_MAX=128
PATH_RESOLVE_INTERACTIVE_QUEUE_MAX=32
PATH_RESOLVE_END_TO_END_TIMEOUT_MS=15000
PATH_RESOLVE_UNIQUE_INFLIGHT_MAX=34
PATH_RESOLVE_OBSERVER_VARIANTS_PER_PACKET_MAX=4
PATH_SINGLE_HISTORY_HOURS=168
PATH_SINGLE_MAX_OBSERVERS=128
PATH_MULTI_HISTORY_WINDOW_HOURS=168
PATH_MULTI_MAX_SCAN_ROWS=2048
PATH_MULTI_MAX_OBSERVERS=128
LAZY_PATH_HISTORY_WINDOW_HOURS=168
LAZY_PATH_MAX_OBSERVATIONS=2048
LAZY_PATH_MAX_OBSERVERS=128
LAZY_PATH_MAX_UNIQUE_HASHES=64
LAZY_PATH_MAX_CANDIDATE_NODES=2048
PATH_LEARNING_MAX_TRAINING_PACKETS=120000
PATH_LEARNING_MAX_NODES=50000
PATH_LEARNING_MAX_LINKS=200000
PATH_LEARNING_MAX_AGGREGATE_KEYS=1000000
PATH_LEARNING_RUN_DEADLINE_MS=2700000
SPAM_MESSAGE_MAX_CANDIDATE_ROWS=10000
SPAM_MESSAGE_MAX_MESSAGES=2000
SPAM_MESSAGE_MAX_OBSERVERS=32
SPAM_MESSAGE_MAX_NORMALIZED_CHARS=512
SPAM_MESSAGE_MAX_INCIDENTS=100
SPAM_MESSAGE_MAX_CANDIDATE_CLUSTERS=64
SPAM_MESSAGE_MAX_EVIDENCE_MEMBERS=256
SPAM_MESSAGE_ANALYSIS_BUDGET_MS=5000
SPAM_MESSAGE_DB_TIMEOUT_MS=15000
STATS_UNIQUE_INFLIGHT_MAX=32
LINK_QUEUE_V3_PRODUCER_ENABLED=1
LINK_QUEUE_V3_MAX_JOBS=5000
LINK_QUEUE_V3_MAX_BYTES=67108864
LINK_QUEUE_V3_MAX_PAYLOAD_BYTES=32768
LINK_QUEUE_V3_DEAD_MAX_JOBS=1000
LINK_QUEUE_V3_DEAD_MAX_BYTES=33554432
LINK_QUEUE_V3_DEAD_RETENTION_MS=2592000000
LINK_QUEUE_V3_MAX_ATTEMPTS=5
LINK_QUEUE_V3_LEASE_MS=120000
LINK_REBUILD_ADMISSION_TIMEOUT_MS=1800000
LINK_REBUILD_COMPLETION_TIMEOUT_MS=21600000
LINK_REBUILD_MAX_NODES=5000
LINK_REBUILD_MAX_PHYSICAL_JOBS=200000
LINK_REBUILD_LEASE_MS=120000
MQTT_AUDIT_MAX_OBSERVATIONS_PER_SCAN=4096
MQTT_AUDIT_MAX_NODES_PER_USER=256
MQTT_AUDIT_RETENTION_DAYS=30
# ── Public map runtime features ───────────────────────────────────────────────
# Public map layers are independently controlled at runtime and fail closed.
# Change these values and recreate only the backend container (no image rebuild);
# connected clients refresh /api/runtime-config within the configured TTL.
PUBLIC_FEATURE_INFERRED_NODES_ENABLED=true
PUBLIC_FEATURE_PACKET_ARCS_ENABLED=true
PUBLIC_FEATURE_HEATMAP_ENABLED=false
PUBLIC_FEATURE_CONFIG_TTL_SECONDS=30
SRTM_MAX_LINK_TILES=64
SRTM_CACHE_MAX_BYTES=21474836480
# Synthetic monitoring and optional alert delivery
SYNTHETIC_INTERVAL_MS=60000
SYNTHETIC_FULL_INTERVAL_MS=720000
SYNTHETIC_SUCCESS_TTL_MS=960000
SYNTHETIC_TIMEOUT_MS=10000
SYNTHETIC_INITIAL_STATE_TIMEOUT_MS=35000
SYNTHETIC_FAILURE_THRESHOLD=3
ALERT_WEBHOOK_URL=
MAINTENANCE_ACTIVE=0
MAINTENANCE_MESSAGE=
# Per-client WebSocket safeguards for browser input and slow consumers.
WS_MAX_PAYLOAD_BYTES=65536
WS_INITIAL_STATE_DB_TIMEOUT_MS=30000
WS_MAX_QUEUE_BYTES=8388608
WS_MAX_BUFFERED_BYTES=33554432
WS_INITIAL_STATE_MAX_BYTES=16777216
# ── Cloudflare Tunnel ─────────────────────────────────────────────────────────
# Get from: Cloudflare Zero Trust → Access → Tunnels → your tunnel → token
CLOUDFLARE_TUNNEL_TOKEN=paste_your_tunnel_token_here
# ── Anubis bot protection ─────────────────────────────────────────────────────
# Signing key shared across all Anubis sidecar containers.
# Generate with: openssl rand -hex 32
# Keep this secret — it signs the PoW challenge cookies.
ANUBIS_ED25519_PRIVATE_KEY_HEX=
# ── App port ──────────────────────────────────────────────────────────────────
PORT=3000
# ── Message-spam detection (Spam Watch) ───────────────────────────────────────
# Detects repeated near-duplicate chat messages and groups them into incidents
# for the public /spam dashboard. All defaults are sensible; see
# docs/spam-detection.md for the full list and tuning guidance.
SPAM_MESSAGE_ANALYZER_ENABLED=true
SPAM_MESSAGE_INTERVAL_MS=300000
SPAM_MESSAGE_WINDOW_HOURS=24
# Min distinct near-duplicate messages before a cluster can be an incident.
# Legit chatter is a few repeats; real spam is dozens+. Raise to reduce false
# positives from small bursts of normal traffic.
SPAM_MESSAGE_MIN_TRANSMISSIONS=8
# Confidence floor for the public list / "ongoing" status (0..1).
SPAM_MESSAGE_PUBLIC_MIN_SCORE=0.5
# At/above this many near-duplicate messages a qualified cluster is treated as a
# sustained automated flood and scored up (unless it is plain test/connectivity
# traffic). Catches templated floods even when they embed the word "test".
SPAM_MESSAGE_FLOOD_MIN_MESSAGES=25
# Channel labels excluded from detection (comma-separated). The test channel
# carries repeated test traffic by design. NOTE: a flood that also hits other
# channels (e.g. Public) is still caught via those copies.
SPAM_MESSAGE_EXCLUDE_CHANNELS=test
# Origin estimation. Primary method resolves relay PATHS: the first repeater in
# the path is the source of truth, resolved next to the closest (lowest-hop)
# receiver. Falls back to the observer-signal estimate (closest-receiver cohort,
# min-hop + slack) when paths can't be resolved. Per-hop km = radius per hop.
SPAM_MESSAGE_ORIGIN_USE_PATHS=true
SPAM_MESSAGE_ORIGIN_PATH_MAX_PACKETS=40
SPAM_MESSAGE_ORIGIN_PATH_MIN_VOTES=3
SPAM_MESSAGE_ORIGIN_PATH_CLUSTER_KM=45
SPAM_MESSAGE_ORIGIN_PATH_AMBIGUOUS_WEIGHT=0.4
SPAM_MESSAGE_ORIGIN_NEAR_HOP_MAX=2
SPAM_MESSAGE_ORIGIN_NEAR_HOP_SLACK=1
SPAM_MESSAGE_ORIGIN_PER_HOP_KM=12
# Disabled ML learner safeguards (used only when the commented Compose service
# is deliberately enabled after a shadow-data run).
MAX_TRAINING_GOLD_ROWS=100000
ML_LEASE_SECONDS=90
ML_HEARTBEAT_SECONDS=20
ML_RUN_DEADLINE_SECONDS=1200
ML_MODEL_VERSION=lightgbm-path-v1
ML_DATA_VERSION=gold-multibyte-v2
# Numeric host group allowed to read the system journal (usually 999).
JOURNAL_GID=999