Node Herder is a smart home management system with Go backend, Vue.js frontend, MQTT broker, and Zigbee2MQTT integration.
Create a .env file in the project root:
# .env (project root)
DATA_ROOT=/opt/nodeherder
Z2M_DEVICE=/dev/serial/by-id/usb-Silicon_Labs_Sonoff_Zigbee_3.0_USB_Dongle_Plus_0001-if00-port0
Z2M_SERIAL_PORT=/dev/ttyUSB0The setup script creates MQTT and Zigbee2MQTT configurations:
./backend/scripts/setup-backend.shWhat it does
- Reads
DATA_ROOTfrom.envat project root - Creates MQTT directories at
${DATA_ROOT}/mqtt/ - Generates random MQTT credentials
- Creates
mosquitto.confand password file - Writes MQTT credentials to
backend/.env - Creates/updates Zigbee2MQTT
configuration.yaml
Example output:
📁 DATA_ROOT = /opt/nodeherder
✅ Generating Mosquitto password...
✅ Password written → /opt/nodeherder/mqtt/config/password.txt
✅ Created → /opt/nodeherder/mqtt/config/mosquitto.conf
✅ MQTT credentials generated
USER = mqttuser
PASS = [randomly generated]
✅ MQTT_URL written:
tcp://mqttuser:[password]@mqtt:1883
docker-compose -f docker-compose.backend.yml up -dThe compose file reads DATA_ROOT, Z2M_DEVICE, and Z2M_SERIAL_PORT from .env to mount volumes.
Location: /path/to/node-herder/.env
# Data directory for MQTT and Zigbee2MQTT (outside repo)
DATA_ROOT=/opt/nodeherder
# Zigbee USB device
Z2M_DEVICE=/dev/serial/by-id/usb-Silicon_Labs_Sonoff_Zigbee_3.0_USB_Dongle_Plus_0001-if00-port0
# Serial port inside Zigbee2MQTT container
Z2M_SERIAL_PORT=/dev/ttyUSB0Variables:
DATA_ROOT- Base directory for MQTT and Z2M data (outside git repo)Z2M_DEVICE- Host USB device path for Zigbee dongleZ2M_SERIAL_PORT- Serial port path inside container (usually/dev/ttyUSB0)
Location: backend/.env
Generated by setup script:
MQTT_USER=mqttuser
MQTT_PASS=<randomly-generated>
MQTT_URL=tcp://mqttuser:<password>@mqtt:1883Location: backend/.env.development
APP_ENV=development
FRONTEND_BASE_URL=http://localhost:4100
OAUTH_CALLBACK_URL=http://localhost:4110/api/auth/callback
JWT_SECRET_KEY=<your-jwt-secret>
SERVICE_CLIENTS=service-a,service-b
SERVICE_SECRET_service-a=<service-a-secret>
SERVICE_SECRET_service-b=<service-b-secret>
OFFLINE_STRICT_LOCAL=trueVariables:
APP_ENV- Environment name:development,staging, orproductionFRONTEND_BASE_URL- Frontend URL for CORS and redirectsOAUTH_CALLBACK_URL- Google OAuth callback URL (use{PORT}as placeholder)JWT_SECRET_KEY- Secret key for JWT token signing (minimum 32 characters)SERVICE_CLIENTS- Comma-separated list of service client IDsSERVICE_SECRET_<ID>- Secret for each service client inSERVICE_CLIENTSOFFLINE_STRICT_LOCAL- Whentrue, local requests use offline auth mode
Generate strong secrets:
openssl rand -base64 32Location: backend/.env.production
APP_ENV=production
FRONTEND_BASE_URL=http://nodeherder.local
OAUTH_CALLBACK_URL=http://nodeherder.local/api/auth/callback
GOOGLE_CLIENT_ID=<your-google-client-id>
GOOGLE_CLIENT_SECRET=<your-google-client-secret>
JWT_SECRET_KEY=<your-jwt-secret>
SERVICE_CLIENTS=service-a,service-b
SERVICE_SECRET_service-a=<service-a-secret>
SERVICE_SECRET_service-b=<service-b-secret>
OFFLINE_STRICT_LOCAL=falseAdditional production variables:
GOOGLE_CLIENT_ID- OAuth 2.0 Client ID from Google Cloud ConsoleGOOGLE_CLIENT_SECRET- OAuth 2.0 Client SecretJWT_SECRET_KEY- Secret key for JWT token signing (minimum 32 characters)SERVICE_CLIENTS- Comma-separated list of service client IDsSERVICE_SECRET_<ID>- Secret for each service client inSERVICE_CLIENTS
Generate strong secrets:
openssl rand -base64 32Node Herder implements the Model Context Protocol (MCP), allowing AI assistants (like Claude or custom LLM proxies) to directly query smart home metrics and discover devices over HTTP.
The MCP server is managed via the Settings -> MCP Server page in the frontend. You can enable/disable it and see the connection status there.
When --mcp is enabled, the following endpoints are available:
| Endpoint | Method | Description |
|---|---|---|
/api/mcp |
POST | JSON-RPC endpoint for MCP requests |
/api/mcp/events |
GET | SSE stream for live resource update notifications |
You can test the MCP server using curl commands.
Discover available resources exposed by the server.
Request:
curl -X POST http://localhost:4110/api/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/list"
}'Fetch the current state of all devices.
Request:
curl -X POST http://localhost:4110/api/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "resources/read",
"params": {
"uri": "nodeherder://devices"
}
}'Execute a tool (e.g., querying device metrics).
Request:
curl -X POST http://localhost:4110/api/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "query_device",
"arguments": {
"target_name": "Living room presence sensor",
"metrics": ["presence"],
"aggregation": "last"
}
}
}'Listen for real-time changes via Server-Sent Events (SSE).
Step 1: Start Listening Open a terminal and run:
curl -N http://localhost:4110/api/mcp/eventsStep 2: Subscribe to Resource In a separate terminal, tell the server you want updates for specific resources:
curl -X POST http://localhost:4110/api/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 5,
"method": "resources/subscribe",
"params": {"uri": "nodeherder://devices"}
}'Result: When a device updates, the SSE stream will emit a notifications/resources/updated event.
-
Resources:
nodeherder://devices: JSON list of all devices and their metrics. Supports live subscriptions.nodeherder://system-prompt: Domain guidance for LLMs.
-
Tools:
query_device: Query metrics with natural language (e.g., "temperature in the kitchen").
Location: frontend/.env.development
VITE_API_BASE_URL=http://localhost:4110/api
VITE_WS_BASE_URL=ws://localhost:4110/ws
PORT=4100Variables:
VITE_API_BASE_URL- Backend API URL (must end with/api)VITE_WS_BASE_URL- WebSocket URL (must end with/ws)PORT- Frontend dev server port
Location: frontend/.env.production
VITE_API_BASE_URL=http://nodeherder.local/api
VITE_WS_BASE_URL=ws://nodeherder.local/wsAfter running setup script:
/opt/nodeherder/ (DATA_ROOT location, outside repo)
├── mqtt/
│ ├── config/
│ │ ├── mosquitto.conf
│ │ └── password.txt
│ ├── data/
│ └── log/
└── zigbee2mqtt-data/
└── configuration.yaml
/path/to/node-herder/ (repo location)
├── .env (Docker Compose config)
├── docker-compose.backend.yml
├── backend/
│ ├── .env (MQTT credentials - auto-generated)
│ ├── .env.development
│ ├── .env.production
│ ├── configs/ (mounted in container)
│ ├── data/ (mounted in container)
│ └── scripts/
│ └── setup-backend.sh
└── frontend/
├── .env.development
└── .env.production
You can also query the backend metrics API directly without going through MCP. This requires authentication.
First, obtain a JWT token using your service credentials (from .env):
export CLIENT_ID="llm-proxy"
export CLIENT_SECRET="<your-service-secret>"
export TOKEN=$(curl -s -X POST http://localhost:4110/api/auth/token \
-H "Content-Type: application/json" \
-d "{\"client_id\": \"$CLIENT_ID\", \"client_secret\": \"$CLIENT_SECRET\"}" | jq -r .access_token)Use the token to query metrics directly:
curl -X POST http://localhost:4110/api/metrics/query \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"deviceIds": ["0xa4c1389b273366c3"],
"exposes": ["alarm"],
"time": { "lookback": "1h" },
"aggregation": "last"
}' | jq-
Create root
.env:echo "DATA_ROOT=$HOME/nodeherder-data" > .env echo "Z2M_DEVICE=/dev/serial/by-id/your-zigbee-dongle" >> .env echo "Z2M_SERIAL_PORT=/dev/ttyUSB0" >> .env
-
Run setup:
./backend/scripts/setup-backend.sh
-
Start backend services:
docker-compose -f docker-compose.backend.yml up -d
-
Run backend locally (optional):
cd backend go run main.go -port 4110 -
Run frontend:
cd frontend npm install npm run dev # Uses .env.development
-
Create root
.envon server:DATA_ROOT=/opt/nodeherder Z2M_DEVICE=/dev/serial/by-id/your-zigbee-dongle Z2M_SERIAL_PORT=/dev/ttyUSB0
-
Create
backend/.env.productionwith secrets:APP_ENV=production FRONTEND_BASE_URL=https://yourdomain.com OAUTH_CALLBACK_URL=https://yourdomain.com/api/auth/callback GOOGLE_CLIENT_ID=your-client-id GOOGLE_CLIENT_SECRET=your-client-secret JWT_SECRET_KEY=your-random-32-char-secret SERVICE_CLIENTS=service-a,service-b SERVICE_SECRET_service-a=your-service-a-secret SERVICE_SECRET_service-b=your-service-b-secret OFFLINE_STRICT_LOCAL=false
-
Run setup:
./backend/scripts/setup-backend.sh
-
Build and start:
docker-compose -f docker-compose.backend.yml up -d --build
- Go to Google Cloud Console
- Navigate to: APIs & Services → Credentials
- Create OAuth 2.0 Client ID (or edit existing)
- Add Authorized JavaScript origins:
http://localhost:4100(development)https://yourdomain.com(production)
- Add Authorized redirect URIs:
http://localhost:4110/api/auth/callback(development)https://yourdomain.com/api/auth/callback(production)
- Copy Client ID and Client Secret to
backend/.env.production
Ensure FRONTEND_BASE_URL in backend matches the actual frontend URL:
- Development:
http://localhost:4100 - Production:
https://yourdomain.com(no port if using proxy)
Check generated credentials:
cat backend/.env | grep MQTTVerify MQTT is running:
docker logs nodeherder-mqttCheck device permissions:
ls -l /dev/serial/by-id/Ensure your user has access to the serial device:
sudo usermod -a -G dialout $USER- Verify redirect URI is authorized in Google Cloud Console
- Check
OAUTH_CALLBACK_URLmatches the actual backend URL - Ensure you're using the correct Client ID/Secret
| Service | Port | Protocol | Purpose |
|---|---|---|---|
| Frontend | 4100 | HTTP | Vue.js dev server |
| Backend | 4110 | HTTP/WS | Go API and WebSocket |
| MQTT | 1883 | MQTT | Internal (not exposed) |
| Zigbee2MQTT UI | 8089 | HTTP | Z2M frontend |
- Never commit
.envfiles to git - they contain secrets backend/.envis auto-generated with random MQTT password- Use strong
JWT_SECRET_KEY(32+ characters, random) - For production, use HTTPS and secure WebSocket (WSS)
- Keep
GOOGLE_CLIENT_SECRETconfidential - Regularly rotate MQTT credentials if exposed