Real-time satellite tracking control system with 3D visualization
Backend, frontend and infrastructure for the LMS mini SLR station.
Single docker compose up deployment. See also lms-hardware for the Raspberry Pi station software.
Real-time 3D tracking — WebGL scene with live position updates at up to 10 Hz, speed-colored trajectory trace and satellite model
MQTT telemetry pipeline — Station → Mosquitto → Backend → InfluxDB + WebSocket → Browser, with polar→cartesian coordinate conversion
Multi-organization — Independent user accounts, invite-code groups, role-based access (owner / admin / member), station isolation per org
Observation sets — Import, overlay and compare multiple tracking sessions as colored traces in the same 3D scene
One-command deploy — Docker Compose with InfluxDB, Mosquitto, Bun backend and Next.js frontend behind Caddy
┌────────────────────────────────────────────────┐
│ Caddy (reverse proxy, TLS) │
│ :80/:443 → /api, /ws → :4000 else → :4001 │
└────────────┬─────────────────────┬─────────────┘
│ │
┌─────────────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Raspberry │ MQTT │ Backend │ │ Frontend │
│ Pi Station │ ──────> │ (Elysia) │ │ (Next.js) │
└─────────────┘ │ :4000 │ │ :4001 │
└──┬──────┬───┘ └─────────────┘
│ │
┌─────▼┐ ┌──▼──────┐
│Influx│ │ SQLite │
│ DB │ │ (Auth) │
└──────┘ └──────────┘
Data flow: Station publishes to MQTT → Backend subscribes, converts polar→cartesian, writes to InfluxDB and broadcasts via WebSocket → Browser renders in Three.js. Commands flow in reverse.
- Docker + Docker Compose
- Caddy (on the host, not in Docker)
- Git
git clone https://github.com/rangelovkiril/lms-controller.git
cd lms-controller
cp .env.example .envGenerate secrets and fill in .env:
# Generate all secrets at once
INFLUX_TOKEN=$(openssl rand -hex 32)
JWT_SECRET=$(openssl rand -hex 32)
INTERNAL_KEY=$(openssl rand -hex 32)
INFLUX_ADMIN_PASSWORD=$(openssl rand -hex 16)
cat > .env << EOF
INFLUX_TOKEN=$INFLUX_TOKEN
INFLUX_ADMIN_PASSWORD=$INFLUX_ADMIN_PASSWORD
JWT_SECRET=$JWT_SECRET
INTERNAL_KEY=$INTERNAL_KEY
EOFdocker compose up -dFirst build takes 2–3 minutes. Wait for all services to be healthy:
docker compose psLocal development:
caddy run --config caddy/CaddyfileOpen http://localhost in your browser.
Production (VPS):
Edit caddy/Caddyfile — comment out the :80 block and uncomment the production block with your domain:
lmsproject.space {
handle /api/* {
reverse_proxy localhost:4000
}
handle /ws {
reverse_proxy localhost:4000
}
handle {
reverse_proxy localhost:4001
}
}
Then run Caddy as a systemd service:
sudo cp caddy/Caddyfile /etc/caddy/Caddyfile
sudo systemctl restart caddyCaddy automatically provisions TLS certificates via Let's Encrypt.
- Register an account at
http://localhost(or your domain) - Create an organization (or join one with an invite code)
- Add a station — save the write-only token shown after creation
- Configure the Raspberry Pi with the token, MQTT broker address and station ID
- Start the hardware client — data appears in the 3D view
# Start only infrastructure
docker compose up -d influxdb mqtt
# Backend (port 3000, auto-restarts on changes)
cd backend && bun install && bun run dev
# Frontend (port 3000, Next.js HMR)
cd frontend && bun install && bun run dev
# Tests
cd backend && bun testWhen running locally without Docker, set BACKEND_URL=http://localhost:3000 for the frontend and make sure Caddy points to the right ports.
Environment variables reference
| Variable | Description |
|---|---|
INFLUX_TOKEN |
InfluxDB admin API token |
INFLUX_ADMIN_PASSWORD |
InfluxDB web UI password (min 8 chars) |
JWT_SECRET |
JWT signing key for user auth |
INTERNAL_KEY |
Server-to-server auth (Next.js SSR → Backend) |
MQTT topics
| Topic | Direction | Payload |
|---|---|---|
slr/<id>/status |
Station → Backend | { "event": "online" | "offline" | "tracking_start" | ... } |
slr/<id>/tracking/<obj>/pos |
Station → Backend | { "az", "el", "dist", "influx_token" } |
slr/<id>/env |
Station → Backend | { "temp", "humidity", "pressure", "wind" } |
slr/<id>/log/<LEVEL> |
Station → Backend | Plain text message |
slr/<id>/cmd |
Backend → Station | { "action": "track" | "stop" } |
Auth model
- Users are independent — register with email + password
- Organizations are groups with 8-character invite codes
- A user can be in multiple organizations (owner / admin / member)
- Stations belong to an organization, scoped via
X-Org-Idheader - JWT (HS256) in
Authorization: Bearerheader X-Internal-Keyfor server-to-server calls (Next.js SSR → Backend)
lms-controller/
├── backend/
│ ├── src/
│ │ ├── index.ts # All routes
│ │ ├── db.ts # SQLite schema
│ │ ├── plugins/ # auth, influx, jwt, mqtt, websocket
│ │ ├── handlers/ # MQTT → InfluxDB + WebSocket
│ │ └── services/ # auth, station, telemetry, observations
│ └── Dockerfile
├── frontend/
│ ├── src/
│ │ ├── app/[locale]/ # Pages (login, register, orgs, stations, ...)
│ │ ├── components/ # visualization/, data-management/, layout/, ui/
│ │ ├── contexts/ # auth, station, observation sets
│ │ ├── hooks/ # useTracking, useWebSocket, useExport, ...
│ │ └── lib/ # API wrapper, stations, tracking
│ ├── messages/ # i18n (en.json, bg.json)
│ └── Dockerfile
├── broker/config/mosquitto.conf
├── caddy/Caddyfile # Reverse proxy config (local + production)
├── docker-compose.yaml
└── .env.example
The current Bun + Elysia backend is a working proof of concept. The long-term plan is to rewrite the backend on the BEAM VM:
- 🔜 Elixir/OTP backend — leveraging lightweight processes, fault tolerance and built-in distribution for handling hundreds of concurrent station connections
- 🔜 EMQX replacing Mosquitto — clustered MQTT broker with native Elixir integration, rule engine and better scalability
- 🔜 LiveView or Phoenix Channels for real-time UI updates without a separate WebSocket layer
The frontend (Next.js + Three.js) and the InfluxDB time-series storage will remain as-is.
GPL v3 — see LICENSE for details.