A live web dashboard for an SvxLink SvxReflector network. It shows who is currently talking, recent activity, talkgroup names and per-callsign information in real time, with a map view of repeaters and hotspots.
The portal is a small Node.js (Express) server that:
- serves the static dashboard from
public/, - serves runtime configuration (title, talkgroup and callsign metadata) as JSON
from
/config.json(plus/talkgroups.jsonand/callsigns.json), - exposes a
/healthzhealth check.
The portal itself does not talk to the SvxReflector and does not proxy
any WebSocket. The dashboard runs in the browser and connects directly to an
upstream live feed (the UPSTREAM_WS_URL below). The Node server only hosts the
static files and the configuration. See the prerequisite next.
You must install and run SVXReflectorFeed first. It connects to your SvxReflector, tracks activity, and publishes the live WebSocket feed that this portal renders.
Install and start it following its own instructions, then note the WebSocket URL it exposes. Because the browser connects to it directly, that URL must be reachable from your users' browsers — not just from the portal server:
SVXReflector ──▶ SVXReflectorFeed ──(WebSocket)──┐
▼
Browser ◀── static files/config ── SVXReflectorPortal
Browser ──────────(WebSocket, direct)────────▶ SVXReflectorFeed
HTTPS note: if you serve the portal over
https://, the feed URL must usewss://(a secure WebSocket). Browsers block insecurews://connections from a secure page (mixed content). Put the feed behind TLS accordingly.
All configuration is supplied through environment variables. For deployment they
are kept in an env.yaml file. Copy the example and edit it:
cp env.yaml.example env.yaml| Variable | Required | Description |
|---|---|---|
UPSTREAM_WS_URL |
yes | WebSocket URL of your SVXReflectorFeed (browser connects to it). |
UI_TITLE |
no | Page / header title. Default: SVX Reflector • Live. |
TG_INFO_JSON |
no | JSON map of talkgroup id → name, e.g. {"9990":"Parrot"}. |
CALLSIGN_INFO_JSON |
no | JSON map of callsign → info text shown on hover / in map popups. |
PORT |
no | Port the server listens on. Default: 8080. |
The talkgroups listed in TG_INFO_JSON also drive the columns shown in the
dashboard table. TG_INFO_JSON and CALLSIGN_INFO_JSON must be valid JSON;
deploy.sh and runlocal.sh validate/echo them so typos surface early.
Requires Node.js 20+, plus yq
and prettier (used by the helper script to load
env.yaml and format sources). On macOS: brew install yq prettier (see
prereq-mac).
npm install
./runlocal.shThe dashboard is then available at http://localhost:8080.
To run without the helper, export the variables yourself and start the server:
export UPSTREAM_WS_URL="wss://feed.example.org/"
npm startTwo supported options: Google Cloud Run (serverless) and a standalone Debian server. Both serve the same app; pick whichever fits your environment.
Container-based, scales to zero, no server to maintain.
Prerequisites:
- The Google Cloud SDK (
gcloud) andyq. On macOS:brew install google-cloud-sdk yq. - A Google Cloud project with billing enabled.
- A configured
env.yaml(see Configuration).
deploy.sh has the project, region and service name hardcoded near
the bottom — edit these to match your setup:
gcloud config set project YOUR_PROJECT_ID
gcloud config set run/region YOUR_REGION # e.g. europe-west1
gcloud run deploy YOUR_SERVICE_NAME ...One-time setup, then deploy:
gcloud auth login
gcloud services enable run.googleapis.com cloudbuild.googleapis.com
./deploy.shdeploy.sh validates the JSON in env.yaml, builds from source (using the
Dockerfile), and deploys to Cloud Run with --allow-unauthenticated.
The command prints the public service URL when it finishes.
Run the portal as a long-lived service on a Debian/Ubuntu host, optionally behind nginx for TLS.
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs gitsudo git clone https://github.com/Guru-RF/SVXReflectorPortal.git /opt/svxreflectorportal
cd /opt/svxreflectorportal
sudo npm install --omit=devsystemd's EnvironmentFile requires KEY=value pairs with single-line
values, so keep each JSON value on one line. Create /etc/svxreflectorportal.env:
UPSTREAM_WS_URL=wss://feed.example.org/
UI_TITLE=SVX Reflector • Live
PORT=8080
TG_INFO_JSON={"9990":"Parrot, test your audio here"}
CALLSIGN_INFO_JSON={"ON0EXAMPLE":"TX:438.8000 RX:431.2000"}Create /etc/systemd/system/svxreflectorportal.service:
[Unit]
Description=SVXReflectorPortal
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/svxreflectorportal
EnvironmentFile=/etc/svxreflectorportal.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
DynamicUser=yes
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now svxreflectorportal
sudo systemctl status svxreflectorportalThe portal now listens on http://<host>:8080.
To serve the portal on port 443 with a domain name, put nginx in front. The portal serves only plain HTTP (no WebSocket of its own), so a basic proxy works:
server {
listen 80;
server_name portal.example.org;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Then add TLS with certbot:
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d portal.example.orgRemember the SVXReflectorFeed WebSocket is a separate service that browsers
connect to directly; expose it over wss:// (its own TLS / reverse proxy) so it
works from the HTTPS portal.
| Path | Purpose |
|---|---|
server.js |
Express server: static hosting + JSON config endpoints. |
public/ |
Dashboard front-end (index.html, app.js, map, styles). |
Dockerfile |
Container image used by Cloud Run. |
deploy.sh |
Cloud Run deploy helper (validates env.yaml). |
runlocal.sh |
Run locally with env.yaml loaded. |
env.yaml |
Your configuration (copied from env.yaml.example). |
MIT © 2026 Joeri Van Dooren