2026-02-11 12:02:40 +00:00
2025-12-03 17:02:57 +00:00
2025-12-03 01:24:42 +00:00
2025-12-02 21:52:34 +00:00
2026-02-11 11:22:03 +00:00

MeshCore Hub

CI Docker codecov BuyMeACoffee

Python 3.14+ platform for managing and orchestrating MeshCore mesh networks.

Warning

BREAKING CHANGES - The latest release replaces Mosquitto with a JWT-based MQTT broker, removes the proprietary receiver service in favor of meshcore-packet-capture, and renames receiver_node_id to observer_node_id in the database. If upgrading from a previous version, see UPGRADING.md for migration steps.

MeshCore Hub Web Dashboard

Important

Help Translate MeshCore Hub 🌍

We need volunteers to translate the web dashboard! Currently only English is available. Check out the Translation Guide to contribute a language pack. Partial translations welcome!

Overview

MeshCore Hub provides a complete solution for monitoring, collecting, and interacting with MeshCore mesh networks. Data ingestion is handled by meshcore-packet-capture, which observes MeshCore RF traffic and publishes decoded packets to MQTT. It consists of multiple components that work together:

Component Description
Collector Subscribes to MQTT events and persists them to a database
API REST API for querying data
Web Dashboard Single Page Application (SPA) for visualizing network status

Architecture

flowchart LR
    subgraph Devices["MeshCore Devices"]
        D1["Device 1"]
        D2["Device 2"]
        D3["Device 3"]
    end

    PCAP["meshcore-packet-capture"]

    D1 -.->|RF| PCAP
    D2 -.->|RF| PCAP
    D3 -.->|RF| PCAP

    PCAP -->|Publish| MQTT["MQTT Broker"]

    subgraph Backend["Backend Services"]
        Collector --> Database --> API
    end

    MQTT --> Collector
    API --> Web["Web Dashboard"]

    style Devices fill:none,stroke:#0288d1,stroke-width:2px
    style PCAP fill:none,stroke:#f57c00,stroke-width:2px
    style Backend fill:none,stroke:#388e3c,stroke-width:2px
    style MQTT fill:none,stroke:#7b1fa2,stroke-width:3px
    style Collector fill:none,stroke:#388e3c,stroke-width:2px
    style Database fill:none,stroke:#c2185b,stroke-width:2px
    style API fill:none,stroke:#1976d2,stroke-width:2px
    style Web fill:none,stroke:#ffa000,stroke-width:2px

Features

  • Event Persistence: Store messages, advertisements, telemetry, and trace data
  • REST API: Query historical data with filtering and pagination
  • Node Tagging: Add custom metadata to nodes for organization
  • Web Dashboard: Visualize network status, node locations, and message history
  • Internationalization: Full i18n support with composable translation patterns
  • Docker Ready: Single image with all components, easy deployment

Getting Started

Docker Compose Profiles

Docker Compose uses profiles to select which services to run. The configuration is split across multiple files:

File Purpose
docker-compose.yml Base shared config (services, profiles, healthchecks, environment)
docker-compose.dev.yml Development overrides (port mappings for direct access)
docker-compose.prod.yml Production overrides (external proxy network, no exposed ports)
docker-compose.traefik.yml Optional Traefik auto-discovery labels

All docker compose commands require explicit file selection with -f:

# Development (default — exposes ports for local access)
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile all up -d

# Production (generic reverse proxy — nginx, caddy, etc.)
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile all up -d

# Production (Traefik)
docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.traefik.yml --profile all up -d

Service profiles:

Profile Services Use Case
all migrate, collector, api, web Everything on one host
core migrate, collector, api, web Central server infrastructure
mqtt meshcore-mqtt-broker Local MQTT broker (optional)
observer packet capture observer Observes RF traffic and publishes to MQTT
seed seed One-time seed data import
migrate migrate One-time database migration

Note: Most deployments connect to an external MQTT broker. Add --profile mqtt only if you need a local broker. The observer profile runs meshcore-packet-capture to observe MeshCore RF traffic and publish decoded packets to MQTT.

# Create database schema
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile migrate run --rm migrate

# Seed the database
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile seed run --rm seed

# Start core services with local MQTT broker
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile mqtt --profile core up -d

# Or connect to external MQTT (configure MQTT_HOST in .env)
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile core up -d

# Start everything including packet capture observer
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile mqtt --profile core --profile observer up -d

# View logs
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile all logs -f

# Stop services
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile all down

Simple Self-Hosted Setup

The quickest way to get started is running the entire stack on a single machine with a connected LoRa radio.

Prerequisites:

  1. A compatible LoRa radio (e.g., Heltec V3, T-Beam) connected via serial

Steps:

# Create a directory, download the Docker Compose files and
# example environment configuration file

mkdir meshcore-hub
cd meshcore-hub
wget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.yml
wget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.dev.yml
wget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/.env.example

# Copy and configure environment
cp .env.example .env
# Edit .env: set PACKETCAPTURE_IATA to your 3-letter airport code
#            set SERIAL_PORT if not /dev/ttyUSB0

# Start the entire stack with local MQTT broker and packet capture
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile mqtt --profile core --profile observer up -d

# View the web dashboard
open http://localhost:8080

This starts all services: MQTT broker, collector, API, web dashboard, and packet capture. The observer profile runs meshcore-packet-capture to observe MeshCore RF traffic and publish decoded packets to MQTT.

Deployment

Production Setup

For production deployments, use docker-compose.prod.yml which connects services to an external proxy network. No ports are exposed directly — all traffic goes through your reverse proxy.

Prerequisites:

  1. A reverse proxy (Nginx Proxy Manager, Caddy, Traefik, etc.)
  2. Docker network for proxy communication

Steps:

# Create proxy network (once)
docker network create proxy-net

# Download compose files and config
mkdir meshcore-hub && cd meshcore-hub
wget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.yml
wget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.prod.yml
wget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/.env.example
cp .env.example .env
# Edit .env: set COMPOSE_PROJECT_NAME, MQTT credentials, API keys, etc.

# Start core services
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile core up -d

# Or include local MQTT broker
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile mqtt --profile core up -d

# Or include packet capture on the same host
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile mqtt --profile core --profile observer up -d

Configure your reverse proxy to forward to the containers:

Service Container Port Path
Web Dashboard {COMPOSE_PROJECT_NAME}-web 8080 /
API {COMPOSE_PROJECT_NAME}-api 8000 /api, /metrics, /health
MQTT WebSocket {COMPOSE_PROJECT_NAME}-mqtt 1883 / (only if using local broker)

Important: Do not host under a subpath (e.g., /meshcore). Proxy at /.

Traefik

A Traefik override file is provided with pre-configured labels:

# Download the Traefik override
wget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.traefik.yml

# Set your domain in .env
echo "TRAEFIK_DOMAIN=meshcore.example.com" >> .env

# Start with Traefik labels
docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.traefik.yml --profile core up -d

This routes the web dashboard and API to TRAEFIK_DOMAIN with automatic TLS.

Adding Remote Observers

Other operators can run their own meshcore-packet-capture instance and publish decoded packets to your MeshCore Hub. They can also optionally contribute to the LetsMesh network.

Prerequisite: Your MQTT broker must be accessible to remote observers. In production, this means exposing the WebSocket listener via a reverse proxy with TLS (e.g., wss://mqtt.example.com/mqtt).

Example: Observer contributing to LetsMesh and your community Hub

# In the observer's .env or docker-compose environment:

# Server 1 - Let's Mesh US (opt-in)
PACKETCAPTURE_MQTT1_ENABLED=true
PACKETCAPTURE_MQTT1_SERVER=mqtt-us-v1.letsmesh.net
PACKETCAPTURE_MQTT1_PORT=443
PACKETCAPTURE_MQTT1_TRANSPORT=websockets
PACKETCAPTURE_MQTT1_USE_TLS=true
PACKETCAPTURE_MQTT1_USE_AUTH_TOKEN=true
PACKETCAPTURE_MQTT1_TOKEN_AUDIENCE=mqtt-us-v1.letsmesh.net

# Server 2 - Let's Mesh EU (opt-in)
PACKETCAPTURE_MQTT2_ENABLED=false

# Server 3 - Your MeshCore Hub
PACKETCAPTURE_MQTT3_ENABLED=true
PACKETCAPTURE_MQTT3_SERVER=mqtt.example.com
PACKETCAPTURE_MQTT3_PORT=443
PACKETCAPTURE_MQTT3_TRANSPORT=websockets
PACKETCAPTURE_MQTT3_USE_TLS=true
PACKETCAPTURE_MQTT3_USE_AUTH_TOKEN=true
PACKETCAPTURE_MQTT3_TOKEN_AUDIENCE=mqtt.example.com

Replace mqtt.example.com with your public MQTT domain. The TOKEN_AUDIENCE must match the MQTT_TOKEN_AUDIENCE or AUTH_EXPECTED_AUDIENCE configured on your broker.

For local network observers (no TLS):

PACKETCAPTURE_MQTT3_SERVER=192.168.1.100
PACKETCAPTURE_MQTT3_PORT=1883
PACKETCAPTURE_MQTT3_TRANSPORT=websockets
PACKETCAPTURE_MQTT3_USE_TLS=false
PACKETCAPTURE_MQTT3_TOKEN_AUDIENCE=mqtt.localhost

Backup & Restore

Using Makefile

# Back up all volumes to backup/
make backup

# Restore a specific volume
make restore FILE=backup/hub_data-20260414-120000.tar.gz

Using shell commands

# Back up the database volume
source .env 2>/dev/null || true
mkdir -p backup
vol=${COMPOSE_PROJECT_NAME:-hub}_data
docker run --rm -v $vol:/data -v $(pwd)/backup:/backup \
  alpine tar czf /backup/$vol-$(date +%Y%m%d-%H%M%S).tar.gz -C / data

# Restore a specific volume (volume name derived from tarball filename)
source .env 2>/dev/null || true
FILE=backup/${COMPOSE_PROJECT_NAME:-hub}_data-20260414-120000.tar.gz
vol=$(basename "$FILE" | sed 's/-[0-9]\{8\}-[0-9]\{6\}\.tar\.gz//')
docker run --rm -v $vol:/data -v $(pwd)/backup:/backup \
  alpine sh -c "cd / && tar xzf /backup/$(basename $FILE)"

Note: Replace hub with your COMPOSE_PROJECT_NAME if using a different instance name. Monitoring infrastructure (Prometheus, Alertmanager) manages its own data — consult your monitoring stack's documentation for backup procedures.

Manual Installation

# Create virtual environment
python -m venv .venv
source .venv/bin/activate

# Install the package
pip install -e ".[dev]"

# Run database migrations
meshcore-hub db upgrade

# Start components (in separate terminals)
meshcore-hub collector
meshcore-hub api
meshcore-hub web

Configuration

All components are configured via environment variables. Create a .env file or export variables:

Common Settings

Variable Default Description
LOG_LEVEL INFO Logging level (DEBUG, INFO, WARNING, ERROR)
DATA_HOME ./data Base directory for runtime data
SEED_HOME ./seed Directory containing seed data files
MQTT_HOST localhost MQTT broker hostname
MQTT_PORT 1883 MQTT broker port
MQTT_USERNAME (none) MQTT username (optional)
MQTT_PASSWORD (none) MQTT password (optional)
MQTT_PREFIX meshcore Topic prefix for all MQTT messages
MQTT_TLS false Enable TLS/SSL for MQTT connection
MQTT_TRANSPORT websockets MQTT transport (tcp or websockets)
MQTT_WS_PATH / MQTT WebSocket path (used when MQTT_TRANSPORT=websockets)

Collector Settings

Variable Default Description
COLLECTOR_CHANNEL_KEYS (none) Additional decoder channel keys (label=hex, label:hex, or hex)
COLLECTOR_INCLUDE_TEST_CHANNEL false Include built-in 'test' channel messages

LetsMesh Packet Decoding

The collector subscribes to packets published by meshcore-packet-capture:

  • <prefix>/+/+/packets
  • <prefix>/+/+/status
  • <prefix>/+/+/internal

Normalization behavior:

  • status packets are stored as informational letsmesh_status events and are not mapped to advertisement rows.
  • Decoder payload type 4 is mapped to advertisement when node identity metadata is present.
  • Decoder payload type 11 (control discover response) is mapped to contact.
  • Decoder payload type 9 is mapped to trace_data.
  • Decoder payload type 8 is mapped to informational path_updated events.
  • Decoder payload type 1 can map to native response events (telemetry_response, battery, path_updated, status_response) when decrypted structured content is available.
  • packet_type=5 packets are mapped to channel_msg_recv.
  • packet_type=1, 2, and 7 packets are mapped to contact_msg_recv when decryptable text is available.
  • For channel packets, if a channel key is available, a channel label is attached (for example Public or #test) for UI display.
  • In the messages feed and dashboard channel sections, known channel indexes are preferred for labels (17 -> Public, 217 -> #test) to avoid stale channel-name mismatches.
  • Additional channel names are loaded from COLLECTOR_CHANNEL_KEYS when entries are provided as label=hex (for example bot=<key>).
  • Decoder-advertisement packets with location metadata update node GPS (lat/lon) for map display.
  • This keeps advertisement listings focused on node advert traffic only, not observer status telemetry.
  • Packets without decryptable message text are kept as informational letsmesh_packet events and are not shown in the messages feed; when decode succeeds the decoded JSON is attached to those packet log events.
  • When decoder output includes a human sender (payload.decoded.decrypted.sender), message text is normalized to Name: Message before storage; receiver/observer names are never used as sender fallback.
  • The collector keeps built-in keys for Public and #test, and merges any additional keys from COLLECTOR_CHANNEL_KEYS.
  • Docker runtime uses the native Python meshcoredecoder library (no external Node.js dependency).

Webhooks

The collector can forward certain events to external HTTP endpoints:

Variable Default Description
WEBHOOK_ADVERTISEMENT_URL (none) Webhook URL for advertisement events
WEBHOOK_ADVERTISEMENT_SECRET (none) Secret sent as X-Webhook-Secret header
WEBHOOK_MESSAGE_URL (none) Webhook URL for all message events
WEBHOOK_MESSAGE_SECRET (none) Secret for message webhook
WEBHOOK_CHANNEL_MESSAGE_URL (none) Override URL for channel messages only
WEBHOOK_CHANNEL_MESSAGE_SECRET (none) Secret for channel message webhook
WEBHOOK_DIRECT_MESSAGE_URL (none) Override URL for direct messages only
WEBHOOK_DIRECT_MESSAGE_SECRET (none) Secret for direct message webhook
WEBHOOK_TIMEOUT 10.0 Request timeout in seconds
WEBHOOK_MAX_RETRIES 3 Max retry attempts on failure
WEBHOOK_RETRY_BACKOFF 2.0 Exponential backoff multiplier

Webhook payload format:

{
  "event_type": "advertisement",
  "public_key": "abc123...",
  "payload": { ... event data ... }
}

Data Retention

The collector automatically cleans up old event data and inactive nodes:

Variable Default Description
DATA_RETENTION_ENABLED true Enable automatic cleanup of old events
DATA_RETENTION_DAYS 30 Days to retain event data
DATA_RETENTION_INTERVAL_HOURS 24 Hours between cleanup runs
NODE_CLEANUP_ENABLED true Enable removal of inactive nodes
NODE_CLEANUP_DAYS 7 Remove nodes not seen for this many days

API Settings

Variable Default Description
API_HOST 0.0.0.0 API bind address
API_PORT 8000 API port
API_READ_KEY (none) Read-only API key
API_ADMIN_KEY (none) Admin API key
METRICS_ENABLED true Enable Prometheus metrics endpoint at /metrics
METRICS_CACHE_TTL 60 Seconds to cache metrics output (reduces database load)

Web Dashboard Settings

Variable Default Description
WEB_HOST 0.0.0.0 Web server bind address
WEB_PORT 8080 Web server port
API_BASE_URL http://localhost:8000 API endpoint URL
API_KEY (none) API key for web dashboard queries (optional)
WEB_THEME dark Default theme (dark or light). Users can override via theme toggle in navbar.
WEB_LOCALE en Locale/language for the web dashboard (e.g., en, es, fr)
WEB_DATETIME_LOCALE en-US Locale used for date formatting in the web dashboard (e.g., en-US for MM/DD/YYYY, en-GB for DD/MM/YYYY).
WEB_AUTO_REFRESH_SECONDS 30 Auto-refresh interval in seconds for list pages (0 to disable)
WEB_ADMIN_ENABLED false Enable admin interface at /a/ (requires auth proxy: X-Forwarded-User/X-Auth-Request-User or forwarded Authorization: Basic ...)
WEB_TRUSTED_PROXY_HOSTS * Comma-separated list of trusted proxy hosts for admin authentication headers. Default: * (all hosts). Recommended: set to your reverse proxy IP in production. A startup warning is emitted when using the default * with admin enabled.
TZ UTC Timezone for displaying dates/times (e.g., America/New_York, Europe/London)
NETWORK_DOMAIN (none) Network domain name (optional)
NETWORK_NAME MeshCore Network Display name for the network
NETWORK_CITY (none) City where network is located
NETWORK_COUNTRY (none) Country code (ISO 3166-1 alpha-2)
NETWORK_RADIO_CONFIG (none) Radio config (comma-delimited: profile,freq,bw,sf,cr,power)
NETWORK_WELCOME_TEXT (none) Custom welcome text for homepage
NETWORK_CONTACT_EMAIL (none) Contact email address
NETWORK_CONTACT_DISCORD (none) Discord server link
NETWORK_CONTACT_GITHUB (none) GitHub repository URL
NETWORK_CONTACT_YOUTUBE (none) YouTube channel URL
CONTENT_HOME ./content Directory containing custom content (pages/, media/)

Timezone handling note:

  • API timestamps that omit an explicit timezone suffix are treated as UTC before rendering in the configured TZ.

Nginx Proxy Manager (NPM) Admin Setup

Use two hostnames so the public map/site stays open while admin stays protected:

  1. Public host: no Access List (normal users).
  2. Admin host: Access List enabled (operators only).

Both proxy hosts should forward to the same web container:

  • Scheme: http
  • Forward Hostname/IP: your MeshCore Hub host
  • Forward Port: 18080 (or your mapped web port)
  • Websockets Support: ON
  • Block Common Exploits: ON

Important:

  • Do not host this app under a subpath (for example /meshcore); proxy it at /.
  • WEB_ADMIN_ENABLED must be true.

In NPM, for the admin host, paste this in the Advanced field:

# Forward authenticated identity for MeshCore Hub admin checks
proxy_set_header Authorization $http_authorization;
proxy_set_header X-Forwarded-User $remote_user;
proxy_set_header X-Auth-Request-User $remote_user;
proxy_set_header X-Forwarded-Email "";
proxy_set_header X-Forwarded-Groups "";

Then attach your NPM Access List (Basic auth users) to that admin host.

Verify auth forwarding:

curl -s -u 'admin:password' "https://admin.example.com/config.js?t=$(date +%s)" \
  | grep -o '"is_authenticated":[^,]*'

Expected:

"is_authenticated": true

If it still shows false, check:

  1. You are using the admin hostname, not the public hostname.
  2. The Access List is attached to that admin host.
  3. The Advanced block above is present exactly.
  4. WEB_ADMIN_ENABLED=true is loaded in the running web container.

Feature Flags

Control which pages are visible in the web dashboard. Disabled features are fully hidden: removed from navigation, return 404 on their routes, and excluded from sitemap/robots.txt.

Variable Default Description
FEATURE_DASHBOARD true Enable the /dashboard page
FEATURE_NODES true Enable the /nodes pages (list, detail, short links)
FEATURE_ADVERTISEMENTS true Enable the /advertisements page
FEATURE_MESSAGES true Enable the /messages page
FEATURE_MAP true Enable the /map page and /map/data endpoint
FEATURE_MEMBERS true Enable the /members page
FEATURE_PAGES true Enable custom markdown pages

Dependencies: Dashboard auto-disables when all of Nodes/Advertisements/Messages are disabled. Map auto-disables when Nodes is disabled.

Custom Content

The web dashboard supports custom content including markdown pages and media files. Content is organized in subdirectories:

Custom logo options:

  • logo.svg — full-color logo, displayed as-is in both themes (no automatic darkening)
  • logo-invert.svg — monochrome/two-tone logo, automatically darkened in light mode for visibility
content/
├── pages/     # Custom markdown pages
│   └── about.md
└── media/     # Custom media files
    └── images/
        ├── logo.svg          # Full-color custom logo (default)
        └── logo-invert.svg   # Monochrome custom logo (darkened in light mode)

Setup:

# Create content directory structure
mkdir -p content/pages content/media

# Create a custom page
cat > content/pages/about.md << 'EOF'
---
title: About Us
slug: about
menu_order: 10
---

# About Our Network

Welcome to our MeshCore mesh network!

## Getting Started

1. Get a compatible LoRa device
2. Flash MeshCore firmware
3. Configure your radio settings
EOF

Frontmatter fields:

Field Default Description
title Filename titlecased Browser tab title and navigation link text (not rendered on page)
slug Filename without .md URL path (e.g., about/pages/about)
menu_order 100 Sort order in navigation (lower = earlier)

The markdown content is rendered as-is, so include your own # Heading if desired.

Pages automatically appear in the navigation menu and sitemap. With Docker, mount the content directory:

# docker-compose.yml (already configured)volumes:
  - ${CONTENT_HOME:-./content}:/content:ro
environment:
  - CONTENT_HOME=/content

Seed Data

The database can be seeded with node tags and network members from YAML files in the SEED_HOME directory (default: ./seed).

Running the Seed Process

Seeding is a separate process and must be run explicitly:

docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile seed up

This imports data from the following files (if they exist):

  • {SEED_HOME}/node_tags.yaml - Node tag definitions
  • {SEED_HOME}/members.yaml - Network member definitions

Directory Structure

seed/                          # SEED_HOME (seed data files)
├── node_tags.yaml            # Node tags for import
└── members.yaml              # Network members for import

data/                          # DATA_HOME (runtime data)
└── collector/
    └── meshcore.db           # SQLite database

Example seed files are provided in example/seed/.

Node Tags

Node tags allow you to attach custom metadata to nodes (e.g., location, role, owner). Tags are stored in the database and returned with node data via the API.

Node Tags YAML Format

Tags are keyed by public key in YAML format:

# Each key is a 64-character hex public key
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:
  name: Gateway Node
  description: Main network gateway
  role: gateway
  lat: 37.7749
  lon: -122.4194
  member_id: alice

fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210:
  name: Oakland Repeater
  elevation: 150

Tag values can be:

  • YAML primitives (auto-detected type): strings, numbers, booleans
  • Explicit type (when you need to force a specific type):
    altitude:
      value: "150"
      type: number
    

Supported types: string, number, boolean

Network Members

Network members represent the people operating nodes in your network. Members can optionally be linked to nodes via their public key.

Members YAML Format

- member_id: walshie86
  name: Walshie
  callsign: Walshie86
  role: member
  description: IPNet Member
- member_id: craig
  name: Craig
  callsign: M7XCN
  role: member
  description: IPNet Member
Field Required Description
member_id Yes Unique identifier for the member
name Yes Member's display name
callsign No Amateur radio callsign
role No Member's role in the network
description No Additional description
contact No Contact information
public_key No Associated node public key (64-char hex)

API Documentation

When running, the API provides interactive documentation at:

Health check endpoints are also available:

Authentication

The API supports optional bearer token authentication:

# Read-only access
curl -H "Authorization: Bearer <API_READ_KEY>" http://localhost:8000/api/v1/nodes

# Admin access
curl -H "Authorization: Bearer <API_ADMIN_KEY>" http://localhost:8000/api/v1/members

Example Endpoints

Method Endpoint Description
GET /api/v1/nodes List all known nodes
GET /api/v1/nodes/{public_key} Get node details
GET /api/v1/nodes/prefix/{prefix} Get node by public key prefix
GET /api/v1/nodes/{public_key}/tags Get node tags
POST /api/v1/nodes/{public_key}/tags Create node tag
GET /api/v1/messages List messages with filters
GET /api/v1/advertisements List advertisements
GET /api/v1/telemetry List telemetry data
GET /api/v1/trace-paths List trace paths
GET /api/v1/members List network members
GET /api/v1/dashboard/stats Get network statistics
GET /api/v1/dashboard/activity Get daily advertisement activity
GET /api/v1/dashboard/message-activity Get daily message activity
GET /api/v1/dashboard/node-count Get cumulative node count history

Development

Setup

# Clone and setup
git clone https://github.com/ipnet-mesh/meshcore-hub.git
cd meshcore-hub
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

# Install pre-commit hooks
pre-commit install

Running Tests

# Run all tests
pytest

# Run with coverage
pytest --cov=meshcore_hub --cov-report=html

# Run specific test file
pytest tests/test_api/test_nodes.py

# Run tests matching pattern
pytest -k "test_list"

Code Quality

# Run all code quality checks (formatting, linting, type checking)
pre-commit run --all-files

Creating Database Migrations

# Auto-generate migration from model changes
meshcore-hub db revision --autogenerate -m "Add new field to nodes"

# Create empty migration
meshcore-hub db revision -m "Custom migration"

# Apply migrations
meshcore-hub db upgrade

Project Structure

meshcore-hub/
├── src/meshcore_hub/       # Main package
│   ├── common/             # Shared code (models, schemas, config)
│   ├── collector/          # MQTT event collector
│   ├── api/                # REST API
│   └── web/                # Web dashboard
│       ├── templates/      # Jinja2 templates (SPA shell)
│       └── static/
│           ├── js/spa/     # SPA frontend (ES modules, lit-html)
│           └── locales/    # Translation files (en.json, languages.md)
├── tests/                  # Test suite
├── alembic/                # Database migrations
├── etc/                    # Configuration files (MQTT, Prometheus, Alertmanager)
├── example/                # Example files for reference
│   ├── seed/               # Example seed data files
│   │   ├── node_tags.yaml  # Example node tags
│   │   └── members.yaml    # Example network members
│   └── content/            # Example custom content
│       ├── pages/          # Example custom pages
│       │   └── join.md     # Example join page
│       └── media/          # Example media files
│           └── images/     # Custom images
├── seed/                   # Seed data directory (SEED_HOME, copy from example/seed/)
├── content/                # Custom content directory (CONTENT_HOME, optional)
│   ├── pages/              # Custom markdown pages
│   └── media/              # Custom media files
│       └── images/         # Custom images (logo.svg/png/jpg/jpeg/webp replace default logo)
├── data/                   # Runtime data directory (DATA_HOME, created at runtime)
├── Dockerfile              # Docker build configuration
├── docker-compose.yml      # Docker Compose base config
├── docker-compose.dev.yml  # Development overrides (port mappings)
├── docker-compose.prod.yml # Production overrides (proxy network)
├── docker-compose.traefik.yml # Optional Traefik labels
├── SCHEMAS.md              # Event schema documentation
├── UPGRADING.md            # Upgrade guide for breaking changes
└── AGENTS.md               # AI assistant guidelines

Documentation

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Run tests and quality checks (pytest && pre-commit run --all-files)
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

License

This project is licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later). See LICENSE for details.

Acknowledgments

Languages
Python 80.5%
JavaScript 16.7%
HTML 1.4%
CSS 0.9%
Dockerfile 0.3%
Other 0.1%