:5000, :7070, :8188) fit together.
Everything the UI needs to reach the backend is configured in
src/environments/environment.ts (and its per-target variants). There is no typed API-client abstraction — every service interpolates `${environment.url}/...` directly. Change the shape of these values and connectivity breaks silently.The four channels at a glance
| Channel | Client service | Direction | Protocol | Env var | Port | Auth |
|---|---|---|---|---|---|---|
| REST API | DroneService, MissionService, … (all HttpClient) | UI → Gateway | HTTP(S) | environment.url | 5000 | Authorization: Bearer <access JWT> via AuthInterceptor |
| Telemetry | WebsocketService | Vehicle → UI | Socket.IO (WebSocket) | environment.url (minus /api/v1) | 5000 | ?token=<access JWT> query param |
| Command (redispad) | VehicleCommandService, ArucoOverlayService | UI → Vehicle (+ ArUco back) | Native WebSocket | environment.ws_proxy | 7070 | ?access_token=<access JWT> query param |
| Video | JanusService | Vehicle → UI | WebRTC (Janus signalling over WS) | environment.janusGatewayUrl | 8188 | Janus VideoRoom pin (currently empty) |
Environment configuration
All four channel targets come from one config object per build target. The values differ per environment, and — critically — the defaultenvironment.ts does not point everything at localhost.
| Key | environment.ts (default/dev) | environment.local.ts | environment.prod.ts | environment.aws-dev.ts |
|---|---|---|---|---|
url | http://localhost:5000/api/v1 | http://localhost:5000/api/v1 | https://prod.skyhub.ai:5000/api/v1 | https://dev.skyhub.ai:5000/api/v1 |
ws_proxy | wss://prod.skyhub.ai:7070 | ws://localhost:7070 | wss://prod.skyhub.ai:7070 | wss://ws_proxy.skyhub-dev.internal:7070 |
janusGatewayUrl | wss://prod.skyhub.ai:8188 | ws://localhost:8188 | wss://prod.skyhub.ai:8188 | wss://dev.skyhub.ai:8188 |
janusIceServers | ['stun:stun.l.google.com:19302'] | (same) | (same) | (same) |
httpSessionExpiryTime | 8 (minutes) | 8 | 8 | 8 |
stripePublishableKey | pk_live_xxx | (absent) | pk_live_xxx | (absent) |
assetsUrl | https://skyhub-prod-assets.s3.eu-central-1.amazonaws.com/drone | (same) | (same) | (same) |
enableIsaacSim | false | false | false | false |
Channel 1 — REST API (:5000)
Every non-realtime interaction is a plain HttpClient call to `${environment.url}/…`. Services never build the base URL themselves beyond that interpolation:
src/app/services/drone-service/drone.service.ts:41
AuthInterceptor (registered as a multi HTTP_INTERCEPTORS provider in app.module.ts) clones every outgoing request to add Authorization: Bearer <access token>, transparently refreshes on 401, and swaps in the refresh token for /auth/refresh. Token lifecycle, the interceptor’s 401 retry, and the 1234 fallback-token quirk are covered in Auth: Guard, Interceptor & AuthService.
The REST surface spans drone CRUD + ~/drone/action/* commands, missions, geofences, assets, executions, calendar, billing, VPN, and Isaac Sim. The full per-service endpoint catalog lives in Angular Services & REST Reference; the server side is HTTP API Overview.
One REST call is really a video control-plane trigger rather than data: startVideoStream asks the Gateway to (re)create the Janus room before the WebRTC channel is used.
src/app/services/drone-service/drone.service.ts:113
The
?update=true query param is load-bearing: the Gateway distinguishes plain start from start-with-update by its presence. stopVideoStream → /video_room/{id}/stop, restartVideoStream → /video_room/{id}/restart.Channel 2 — Socket.IO telemetry (:5000)
WebsocketService receives live telemetry from the vehicle. It reuses the Gateway host but connects to the root namespace, so it strips /api/v1 off environment.url to derive the base:
src/app/services/websocket.service.ts:64
token query param (not access_token, and not a Bearer header). After connecting, the UI emits subscribe_telemetry { drone_id, stream_type } and the Gateway pushes telemetry_data { type, drone_id, data } events, which handleTelemetryData routes by data.type into per-stream RxJS Subjects (gpsSubject, diagnosticsSubject, relAltSubject, logsSubject, homePositionSubject, gpsRawSubject, cameraStatusSubject, arucoTrackingSubject). The routing keys are the TELEMETRY_DATA_TYPES enum in src/app/shared/constants/telemetry.constant.ts.
On every
connect (including reconnects), WebsocketService replays activeSubscriptions because the server loses room membership on a transport drop (websocket.service.ts:81). Socket handlers run via ngZone.runOutsideAngular; only subject emissions re-enter the zone via emitInZone. The full client behavior — stream types, zone handling, subscription replay — is in Real-time Telemetry Client, and the server side in Socket.IO Telemetry Streaming.Channel 3 — redispad command WebSocket (:7070)
Low-latency UI → vehicle commands (gamepad, guided-control, LAND, camera/gimbal) travel over a raw WebSocket to the separate redispad proxy, keyed by drone id, with the JWT in the access_token query param:
src/app/services/vehicle-command.service.ts:63
VehicleCommandService sends JSON frames — gamepad { buttons, axes, timestamp, front_ts }, { type: 'guided_control', command }, { type: 'camera_command', … }, and { set_mode: 'LAND' } — and re-emits inbound JSON (guided_control_status, camera_status, video_stream_state) on its message$ Subject. It auto-reconnects after 1s unless the disconnect was intentional or errored, and guards against stale sockets by capturing the socket per handler closure.
The redispad proxy resolves the drone IP from PostgreSQL and bridges to Redis ({drone_ip}:gamepad_input out, {ip}:output / {ip}:aruco_tracking back). It is a distinct service from the Gateway — see WebSocket Gamepad Proxy and the client-side command/gamepad model in Vehicle Commands & Gamepad (redispad).
Channel 4 — Janus video (:8188)
Live camera video is WebRTC, out-of-band from the Gateway. JanusService is a janus.plugin.videoroom subscriber pointed at environment.janusGatewayUrl with ICE from environment.janusIceServers:
src/app/services/janus.service.ts:11
startVideoStream), after which JanusService joins the room (room = video_room_id, pin = video_room_password), subscribes to publisher streams, and attaches remote tracks to a <video> element, exposing videoStreamStatus$. Recorded clips are played back separately as HLS (m3u8 from AssetService). See App State & Video (Janus/WebRTC), Video, Gimbal & Manual Flight Control, and the SFU itself in Janus WebRTC SFU.
How the ports map in production
Locally the three ports are justlocalhost:{5000,7070,8188}. In production the browser connects to a single public host (prod.skyhub.ai) on all three ports, and the WireGuard jumphost’s nginx TLS-terminates each and proxies inward to the internal service (e.g. :5000 → gateway.skyhub-prod.internal:5000, :8188 → the Janus signalling WebSocket). This funnel-through-one-host topology is described in Network & VPN Topology and VPC, WireGuard Jumphost & nginx Routing.
Port 5000
Gateway — REST (
/api/v1) and Socket.IO telemetry (root namespace) share this port.Port 7070
WS Proxy (redispad) — command channel + ArUco overlay. Not the Gateway.
Port 8188
Janus SFU — WebRTC signalling for live video subscribe.
Gotchas a future editor must preserve
Telemetry base URL is derived, not configured
Telemetry base URL is derived, not configured
WebsocketService computes its host as environment.url.replace('/api/v1', ''). There is no separate telemetry URL key — changing the REST path shape silently breaks telemetry.Three different auth-token placements
Three different auth-token placements
REST uses an
Authorization: Bearer header (via AuthInterceptor); Socket.IO uses a token query param; redispad uses an access_token query param. All three carry the same localStorage access JWT, but the placement/name differs per channel.Two redispad sockets per drone
Two redispad sockets per drone
VehicleCommandService and ArucoOverlayService each open their own WebSocket to /redispad/{droneId}. Expect two connections, not one.redispad ≠ Gateway
redispad ≠ Gateway
Port 7070 is a separate Redis relay (WS Proxy); the Gateway repo does not own it. Telemetry (5000) and commands (7070) must never be merged.
Default env points partly at prod
Default env points partly at prod
environment.ts combines a localhost REST URL with production ws_proxy/janus/Stripe values. Use environment.local.ts for a fully-local stack.CAMERA_STATUS / ARUCO_TRACKING arrive on two channels
CAMERA_STATUS / ARUCO_TRACKING arrive on two channels
Both can come via Socket.IO telemetry (
cameraStatusSubject / arucoTrackingSubject) and via redispad (camera_status on message$; aruco_tracking on the overlay socket). Sources can interleave.Related pages
Real-time Telemetry Client
Deep-dive on the Socket.IO channel: stream types, routing, reconnect replay.
Vehicle Commands & Gamepad
The redispad command channel, guided-control gating, and gamepad payloads.
App State & Video
JanusService WebRTC subscribe lifecycle and HLS asset playback.
Auth: Guard, Interceptor & AuthService
JWT lifecycle, Bearer injection, and the
401 refresh retry.Angular Services & REST Reference
Every service and the Gateway endpoints it calls.
Real-time Transport Channels
Platform-wide view of the concurrent realtime transports.

