# MeshCore Hub - Environment Configuration # Copy this file to .env and customize values # # Configuration is grouped by service. Most deployments only need: # - Common Settings (always required) # - MQTT Settings (always required) # - Packet Capture Settings (for observer nodes) # # The Collector, API, and Web services typically run as a combined "core" # profile and share the same data directory. # # ----------------------------------------------------------------------------- # QUICK START: Observer Node # ----------------------------------------------------------------------------- # For an observer node capturing mesh traffic, you need: # # MQTT_HOST=your-mqtt-broker.example.com # MQTT_PORT=1883 # MQTT_USERNAME=your_username # MQTT_PASSWORD=your_password # MQTT_TLS=false # SERIAL_PORT=/dev/ttyUSB0 # # Serial ports are typically /dev/ttyUSB[0-9] or /dev/ttyACM[0-9] on Linux. # ----------------------------------------------------------------------------- # ----------------------------------------------------------------------------- # ============================================================================= # COMMON SETTINGS # ============================================================================= # These settings apply to all services # Docker Compose project name # Used as a prefix for container names (e.g., hub-api) and volume names # (e.g., hub_data). Change per instance when running multiple deployments # on the same Docker host (e.g., hub-prod, hub-beta, hub-stg). # For multi-instance setups, see docs/deployment.md. COMPOSE_PROJECT_NAME=hub # Domain name for this instance (only needed for Traefik deployments) # Used by docker-compose.traefik.yml to configure routing rules # TRAEFIK_DOMAIN=meshcore.example.com # Router priority for Traefik (higher = matched first) # Use a higher value for more specific subdomain instances to avoid # conflicts with wildcard routes from other instances on the same host. # Production: 10 (default) # Staging: 20 # MQTT broker: 30 # TRAEFIK_PRIORITY=10 # Docker image version tag to use # Options: latest, main, v1.0.0, etc. IMAGE_VERSION=latest # Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) LOG_LEVEL=INFO # Base directory for runtime data (database, etc.) # Default: ./data (relative to docker-compose.yml location) # Inside containers this is mapped to /data # # Structure: # ${DATA_HOME}/ # └── collector/ # └── meshcore.db # SQLite database DATA_HOME=./data # ----------------------------------------------------------------------------- # Database # ----------------------------------------------------------------------------- # SQLite is the zero-config default and needs nothing here — it lives at # ${DATA_HOME}/collector/meshcore.db. # # To use PostgreSQL instead, set DATABASE_BACKEND=postgres and fill in the # DATABASE_* values below (the bundled postgres container derives its # POSTGRES_USER/PASSWORD/DB from DATABASE_USER/PASSWORD/NAME). You must also # activate the compose 'postgres' profile, e.g. `docker compose --profile postgres up`. # # See docs/database.md for the full backend reference: production role/database # provisioning, managed/external Postgres, and schema-per-instance (search_path) # isolation for multiple instances sharing one cluster. # # DATABASE_BACKEND=postgres # DATABASE_HOST=postgres # DATABASE_PORT=5432 # DATABASE_NAME=meshcorehub # DATABASE_SCHEMA=meshcorehub # override per instance (e.g. prod, stg) on a shared cluster # DATABASE_USER=meshcorehub # DATABASE_PASSWORD= # required for postgres; e.g. `openssl rand -base64 32` # # Advanced: set DATABASE_URL to a full SQLAlchemy URL to override all of the above # (e.g. a managed/external Postgres). Takes precedence over DATABASE_BACKEND. # DATABASE_URL=postgresql+psycopg2://user:pass@host:5432/dbname # Directory containing seed data files for import # Default: ./seed (relative to docker-compose.yml location) # Inside containers this is mapped to /seed # # Structure: # ${SEED_HOME}/ # └── node_tags.yaml # Node tags for import SEED_HOME=./seed # ============================================================================= # MQTT SETTINGS # ============================================================================= # MQTT broker connection settings for collector and API services # Uses the MeshCore MQTT broker (WebSocket transport with subscriber auth) # See: https://github.com/michaelhart/meshcore-mqtt-broker # MQTT Broker host # When using the local MQTT broker (--profile mqtt), use "mqtt" # When using an external broker, set the hostname/IP # For native (non-Docker) installs, use "localhost" (the Python default) MQTT_HOST=mqtt # MQTT Broker port # Default: 1883 (local, plain WebSocket) # Production behind reverse proxy: 8883 (TLS) MQTT_PORT=1883 # MQTT subscriber authentication # The broker uses subscriber accounts with roles: # Role 1 (admin): full access including /internal topics # Role 2 (full_access): all public topics, no filtering # Role 3 (limited): filtered data (SNR, RSSI, etc. removed) MQTT_USERNAME= MQTT_PASSWORD= # MQTT topic prefix for all MeshCore messages MQTT_PREFIX=meshcore # Enable TLS/SSL for MQTT connection # Set to true when connecting via wss:// (e.g., behind a reverse proxy) MQTT_TLS=false # MQTT transport protocol # The MeshCore MQTT broker uses WebSockets exclusively MQTT_TRANSPORT=websockets # MQTT WebSocket path (used when MQTT_TRANSPORT=websockets) # Default: / (broker accepts any path) # Production: can be set to /mqtt if reverse proxy rewrites paths MQTT_WS_PATH=/ # JWT audience claim for packet capture authentication tokens # Must match AUTH_EXPECTED_AUDIENCE on the broker # Local default: mqtt.localhost # Production: set to your broker's domain (e.g., mqtt.example.com) MQTT_TOKEN_AUDIENCE=mqtt.localhost # ============================================================================= # PACKET CAPTURE SETTINGS # ============================================================================= # External packet capture service (ghcr.io/agessaman/meshcore-packet-capture) # See https://github.com/agessaman/meshcore-packet-capture for documentation. # Uses the "observer" compose profile. # Publishes captured packets to MQTT in LetsMesh upload format, ingested by # the collector. # Serial port for the packet capture device SERIAL_PORT=/dev/ttyUSB0 # Docker image version tag for the packet capture image PACKETCAPTURE_IMAGE_VERSION=latest # Connection timeout and retry settings PACKETCAPTURE_TIMEOUT=30 PACKETCAPTURE_MAX_CONNECTION_RETRIES=5 PACKETCAPTURE_CONNECTION_RETRY_DELAY=5 PACKETCAPTURE_HEALTH_CHECK_INTERVAL=30 # IATA airport code identifier (used in Let's Mesh topic templates) PACKETCAPTURE_IATA=LOC # Device display name (optional, defaults to device name from meshcore connection) # PACKETCAPTURE_ORIGIN= # Send flood adverts at this interval in hours (0 = disabled) PACKETCAPTURE_ADVERT_INTERVAL_HOURS=11 # RF data cache timeout in seconds PACKETCAPTURE_RF_DATA_TIMEOUT=15.0 # ------------------- # Let's Mesh MQTT Brokers (opt-in) # ------------------- # Enable to publish captured packets to the Let's Mesh cloud map # Broker 1 - Let's Mesh US PACKETCAPTURE_MQTT1_ENABLED=false PACKETCAPTURE_MQTT1_SERVER=mqtt-us-v1.letsmesh.net PACKETCAPTURE_MQTT1_PORT=443 PACKETCAPTURE_MQTT1_USE_TLS=true PACKETCAPTURE_MQTT1_USE_AUTH_TOKEN=true PACKETCAPTURE_MQTT1_TOKEN_AUDIENCE=mqtt-us-v1.letsmesh.net PACKETCAPTURE_MQTT1_KEEPALIVE=120 # Broker 2 - Let's Mesh EU PACKETCAPTURE_MQTT2_ENABLED=false PACKETCAPTURE_MQTT2_SERVER=mqtt-eu-v1.letsmesh.net PACKETCAPTURE_MQTT2_PORT=443 PACKETCAPTURE_MQTT2_USE_TLS=true PACKETCAPTURE_MQTT2_USE_AUTH_TOKEN=true PACKETCAPTURE_MQTT2_TOKEN_AUDIENCE=mqtt-eu-v1.letsmesh.net PACKETCAPTURE_MQTT2_KEEPALIVE=120 # Broker 3 - Local MQTT (enabled by default, wired to hub's MQTT_* settings) # Uses websockets and auth tokens by default (set in docker-compose.yml) PACKETCAPTURE_MQTT3_ENABLED=true PACKETCAPTURE_MQTT3_KEEPALIVE=60 # MQTT reconnection settings PACKETCAPTURE_MAX_MQTT_RETRIES=5 PACKETCAPTURE_MQTT_RETRY_DELAY=5 PACKETCAPTURE_EXIT_ON_RECONNECT_FAIL=true # ============================================================================= # COLLECTOR SETTINGS # ============================================================================= # The collector subscribes to MQTT events and stores them in the database # Refresh interval for reloading channel keys from the database (seconds). # CHANNEL_REFRESH_INTERVAL_SECONDS=300 # ------------------- # Webhook Settings # ------------------- # Webhooks forward mesh events to external HTTP endpoints as POST requests # Webhook for advertisement events (node discovery) WEBHOOK_ADVERTISEMENT_URL= WEBHOOK_ADVERTISEMENT_SECRET= # Webhook for all message events (channel and direct messages) WEBHOOK_MESSAGE_URL= WEBHOOK_MESSAGE_SECRET= # Optional: Separate URLs for channel vs direct messages # These override WEBHOOK_MESSAGE_URL if set # WEBHOOK_CHANNEL_MESSAGE_URL= # WEBHOOK_CHANNEL_MESSAGE_SECRET= # WEBHOOK_DIRECT_MESSAGE_URL= # WEBHOOK_DIRECT_MESSAGE_SECRET= # Webhook behavior settings WEBHOOK_TIMEOUT=10.0 WEBHOOK_MAX_RETRIES=3 WEBHOOK_RETRY_BACKOFF=2.0 # ------------------- # Data Retention Settings # ------------------- # Automatic cleanup of old event data (advertisements, messages, telemetry, etc.) # Enable automatic cleanup of old event data DATA_RETENTION_ENABLED=true # Number of days to retain event data # Events older than this are deleted during cleanup DATA_RETENTION_DAYS=30 # Hours between automatic cleanup runs # Applies to both event data and node cleanup DATA_RETENTION_INTERVAL_HOURS=24 # ------------------- # Raw Packet Capture # ------------------- # Capture every inbound packets-feed packet into the raw_packets table. # Off by default. In Docker Compose this is derived from FEATURE_PACKETS, so # setting FEATURE_PACKETS=true enables both capture and the Packets page. # RAW_PACKET_CAPTURE_ENABLED=false # Days to retain raw packets before cleanup. Defaults to 7, independent of # DATA_RETENTION_DAYS. The raw_packets table grows fastest of all; lower this on # busy meshes or constrained storage. Retention runs regardless of capture being # enabled, so disabling capture lets existing rows drain. # RAW_PACKET_RETENTION_DAYS=7 # ------------------------- # Observer Ingestion Filter # ------------------------- # Restrict which remote observers may ingest events, keyed on the observer's # public key (the segment of its LetsMesh upload topic). Both lists # are comma-separated. The allowlist takes precedence over the denylist, and # matching is case-insensitive PREFIX matching (a full 64-char key or a shorter # prefix both work). Blocked packets are dropped before any decode or DB write. # Leave both empty to accept all observers (default). # OBSERVER_ALLOWLIST= # OBSERVER_DENYLIST= # ------------------- # Spam Detection # ------------------- # Score each message's spam likelihood at ingest and hide likely-spam by default # (with a "show potential spam" toggle on the Messages page). Nothing is ever # dropped; the score is stored and the display layer filters on it. # # On by default. FEATURE_SPAM_DETECTION is the single switch operators set: in # Docker Compose it drives the backend SPAM_DETECTION_ENABLED for the collector # (scoring + sweep) and the api (hide-filter), and exposes the UI toggle. Opt out # by setting it to false. # FEATURE_SPAM_DETECTION=true # Backend operational switch, read by the collector + api. Compose derives it from # FEATURE_SPAM_DETECTION (SPAM_DETECTION_ENABLED=${FEATURE_SPAM_DETECTION}); set it # directly only when running the services without Compose. # SPAM_DETECTION_ENABLED=true # Score at/above which a message is treated as likely spam (hidden by default in # the API, logged at WARNING by the collector). Read by the collector + api. # SPAM_SCORE_THRESHOLD=0.65 # Scoring tuning (collector only; only consulted when detection is enabled). # SPAM_WINDOW_SECONDS=300 # sliding window for frequency counts # SPAM_PATH_HOPS=3 # leading origin-side hops that form the prefix # SPAM_MIN_PATH_HOPS=3 # min path_len before the path signal applies # SPAM_PATH_THRESHOLD=6 # joint path+sender count that saturates the path signal # SPAM_NAME_THRESHOLD=10 # sender count that saturates the name signal # SPAM_WEIGHT_PATH=0.75 # weight of the path signal # SPAM_WEIGHT_NAME=0.25 # weight of the name signal # SPAM_RESCORE_INTERVAL_SECONDS=120 # background re-scoring sweep cadence (0 disables) # ------------------- # Node Cleanup Settings # ------------------- # Automatic removal of inactive nodes # Enable automatic cleanup of inactive nodes # Nodes with last_seen=NULL (never seen on network) are NOT removed NODE_CLEANUP_ENABLED=true # Remove nodes not seen for this many days (based on last_seen field) NODE_CLEANUP_DAYS=30 # ============================================================================= # API SETTINGS # ============================================================================= # REST API for querying data # External API port API_PORT=8000 # Number of API worker processes (default 1). Increase to use multiple CPU # cores under load — each worker is an independent process sharing the same # listening socket. All workers read configuration from these env vars. # API_WORKERS=1 # API Keys for authentication # Generate secure keys for production: openssl rand -hex 32 # Leave empty to disable authentication (not recommended for production) API_READ_KEY= API_ADMIN_KEY= # ------------------- # Prometheus Metrics # ------------------- # Prometheus metrics endpoint exposed at /metrics on the API service # Enable Prometheus metrics endpoint # Default: true METRICS_ENABLED=true # Seconds to cache metrics output (reduces database load) # Default: 60 METRICS_CACHE_TTL=60 # CORS origins for the API server (comma-separated) # Only needed when running the web dashboard on a different origin than the API # Example: http://localhost:8080,http://localhost:3000 # CORS_ORIGINS= # External Prometheus port (when using --profile metrics) PROMETHEUS_PORT=9090 # ------------------- # Redis Cache # ------------------- # Optional Redis cache for API response caching. # Reduces database load for read-heavy endpoints (nodes, messages, dashboard). # When disabled or unavailable, the API queries the database directly. # # Docker: Redis is included in the "cache" profile (--profile cache). # REDIS_ENABLED defaults to false everywhere. To enable, set REDIS_ENABLED=true # and start with --profile cache (or point REDIS_HOST at an external Redis). # Bare-metal: Install Redis separately and set REDIS_ENABLED=true. # # For multi-instance setups sharing one Redis, use different REDIS_KEY_PREFIX # values per instance (e.g., hub for prod, hub-stg for staging). # Enable Redis caching (default: false outside Docker, true in Docker Compose) # REDIS_ENABLED=false # Redis server host (use "redis" in Docker Compose) # REDIS_HOST=localhost # Redis server port # REDIS_PORT=6379 # Redis database number # REDIS_DB=0 # Redis password (optional) # REDIS_PASSWORD= # Cache key prefix for multi-instance isolation # REDIS_KEY_PREFIX=hub # Default cache TTL in seconds (matches web auto-refresh interval) # REDIS_CACHE_TTL=30 # Cache TTL for dashboard endpoints (seconds) # REDIS_CACHE_TTL_DASHBOARD=30 # External Alertmanager port (when using --profile metrics) ALERTMANAGER_PORT=9093 # ============================================================================= # WEB DASHBOARD SETTINGS # ============================================================================= # Web interface for visualizing network status # External web port WEB_PORT=8080 # API endpoint URL for the web dashboard # Default: http://localhost:8000 # API_BASE_URL=http://localhost:8000 # API key for web dashboard queries (optional) # If API_READ_KEY is set on the API, provide it here # API_KEY= # Default theme for the web dashboard (dark or light) # Users can override via the theme toggle; their preference is saved in localStorage # Default: dark # WEB_THEME=dark # Locale/language for the web dashboard # Default: en # Supported: en, nl (see src/meshcore_hub/web/static/locales/ for available translations) # WEB_LOCALE=en # Locale used for date/time formatting in the web dashboard # Controls date ordering only; 24-hour clock is still used by default # Examples: en-US (MM/DD/YYYY), en-GB (DD/MM/YYYY) # Default: en-US # WEB_DATETIME_LOCALE=en-US # Auto-refresh interval in seconds for list pages (nodes, advertisements, messages) # Set to 0 to disable auto-refresh # Default: 30 # WEB_AUTO_REFRESH_SECONDS=30 # Enable debug mode in the web dashboard # Shows extra diagnostic info (e.g., raw user IDs in the user menu) # Default: false # WEB_DEBUG=false # ------------------- # OIDC Authentication # ------------------- # Enable OIDC/OAuth2 authentication for the web dashboard. # When enabled, the admin interface (/a/) requires authenticated sessions. # Requires an OIDC-compliant identity provider (e.g., LogTo, Keycloak). # Enable OIDC authentication # Default: false # OIDC_ENABLED=false # OIDC client ID (from your IdP) # OIDC_CLIENT_ID= # OIDC client secret (from your IdP) # OIDC_CLIENT_SECRET= # OIDC discovery base URL (.well-known/openid-configuration is appended automatically) # OIDC_DISCOVERY_URL= # OIDC callback URL (overrides auto-derivation from request) # Example: https://hub.example.com/auth/callback # OIDC_REDIRECT_URI= # Post-logout redirect URI (must match Sign-out redirect URIs configured in IdP) # Falls back to OIDC_REDIRECT_URI base or request.base_url if not set # Example: https://hub.example.com/ # OIDC_POST_LOGOUT_REDIRECT_URI= # OAuth scopes to request. The 'openid' scope is required for ID tokens # and userinfo endpoint access. Quotes around the value are stripped automatically. # Default: openid email profile # OIDC_SCOPES="openid email profile" # ID token claim name containing user roles # Default: roles # OIDC_ROLES_CLAIM=roles # IdP role name that grants admin access # Default: admin # OIDC_ROLE_ADMIN=admin # IdP role name for operator access (future use) # Default: operator # OIDC_ROLE_OPERATOR=operator # IdP role name for member access # Default: member # OIDC_ROLE_MEMBER=member # IdP role name for test users (excluded from public member views and counts) # Default: test # OIDC_ROLE_TEST=test # Secret for signing session cookies (required when OIDC_ENABLED=true) # Generate with: openssl rand -hex 32 # OIDC_SESSION_SECRET= # Session cookie lifetime in seconds # Default: 86400 (24 hours) # OIDC_SESSION_MAX_AGE=86400 # HTTPS-only session cookies (enable in production behind TLS) # Default: false # OIDC_COOKIE_SECURE=false # Timezone for displaying dates/times on the web dashboard # Uses standard IANA timezone names (e.g., America/New_York, Europe/London) # Default: UTC TZ=UTC # Directory containing custom content (pages/, media/) # Default: ./content # CONTENT_HOME=./content # ------------------- # Network Information # ------------------- # Displayed on the web dashboard homepage # Network domain name (optional) # NETWORK_DOMAIN= # Network display name NETWORK_NAME=MeshCore Network # Network location NETWORK_CITY= NETWORK_COUNTRY= # Radio configuration (six individual variables — no unit suffixes needed) # Units (MHz, kHz, dBm) are applied automatically on display NETWORK_RADIO_PROFILE=EU/UK Narrow NETWORK_RADIO_FREQUENCY=869.618 NETWORK_RADIO_BANDWIDTH=62.5 NETWORK_RADIO_SPREADING_FACTOR=8 NETWORK_RADIO_CODING_RATE=8 NETWORK_RADIO_TX_POWER=22 # Welcome text displayed on the homepage (optional, plain text) # If not set, a default welcome message is shown NETWORK_WELCOME_TEXT= # Flash banner announcement displayed on all pages (optional, Markdown supported) # Supports bold, italic, links, inline code. Empty = no banner shown. # Example: **Maintenance** scheduled for Saturday — see [details](https://example.com) NETWORK_ANNOUNCEMENT= # System announcement banner (optional, Markdown supported) # Non-dismissable banner shown above the network announcement on every page, # for important system notices (downtime, maintenance windows, alerts). # Stays visible until unset and the web service is restarted. Empty = no banner. SYSTEM_ANNOUNCEMENT= # Maintenance mode (default: false) # When true, disables almost all site functionality: the nav shows only Home, # the user/profile menu is hidden, and every page renders a "Site Under # Maintenance" notice. No backend API calls are made, so the API/database can # be offline while the web component keeps running. Requires a web restart. SYSTEM_MAINTENANCE=false # ------------------- # Feature Flags # ------------------- # Control which pages are visible in the web dashboard # Set to false to completely hide a page (nav, routes, sitemap, robots.txt) # FEATURE_DASHBOARD=true # FEATURE_NODES=true # FEATURE_ADVERTISEMENTS=true # FEATURE_MESSAGES=true # FEATURE_MAP=true # FEATURE_MEMBERS=true # FEATURE_PAGES=true # FEATURE_CHANNELS=true # FEATURE_RADIO_CONFIG=true # Packets page is ON by default. This var also drives raw-packet capture # on the collector via Compose (RAW_PACKET_CAPTURE_ENABLED=${FEATURE_PACKETS}). # FEATURE_PACKETS=true # Spam detection is ON by default. This var also drives the backend scoring + # hide switch on the collector/api via Compose # (SPAM_DETECTION_ENABLED=${FEATURE_SPAM_DETECTION}). Set to false to opt out. # See the Spam Detection section above for the scoring tuning vars. # FEATURE_SPAM_DETECTION=true # ------------------- # Contact Information # ------------------- # Contact links displayed in the footer NETWORK_CONTACT_EMAIL= NETWORK_CONTACT_DISCORD= NETWORK_CONTACT_GITHUB= NETWORK_CONTACT_YOUTUBE=