The Angular Dashboard talks to the backend over four distinct transport channels, each with its own service, direction, protocol, target port, and authentication. They are deliberately separate — conflating them is the single easiest way to break the client. This page is the map of that integration surface: what each channel is, which environment variable configures it, and how the three network endpoints (: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

ChannelClient serviceDirectionProtocolEnv varPortAuth
REST APIDroneService, MissionService, … (all HttpClient)UI → GatewayHTTP(S)environment.url5000Authorization: Bearer <access JWT> via AuthInterceptor
TelemetryWebsocketServiceVehicle → UISocket.IO (WebSocket)environment.url (minus /api/v1)5000?token=<access JWT> query param
Command (redispad)VehicleCommandService, ArucoOverlayServiceUI → Vehicle (+ ArUco back)Native WebSocketenvironment.ws_proxy7070?access_token=<access JWT> query param
VideoJanusServiceVehicle → UIWebRTC (Janus signalling over WS)environment.janusGatewayUrl8188Janus VideoRoom pin (currently empty)
Two of these channels look like the same thing but are not. Telemetry (WebsocketService, Socket.IO on :5000) is served by the Gateway. The redispad command channel (VehicleCommandService / ArucoOverlayService, raw WebSocket on :7070) is served by a separate Redis-backed relay — the WebSocket Gamepad Proxy — which the Gateway repo does not own. A refactor must keep them distinct.
The four services are the whole client-side integration boundary; every feature component reaches the backend through one of them. Deep-dives on each live in sibling pages — this page focuses on the transport plumbing they share.

Environment configuration

All four channel targets come from one config object per build target. The values differ per environment, and — critically — the default environment.ts does not point everything at localhost.
Keyenvironment.ts (default/dev)environment.local.tsenvironment.prod.tsenvironment.aws-dev.ts
urlhttp://localhost:5000/api/v1http://localhost:5000/api/v1https://prod.skyhub.ai:5000/api/v1https://dev.skyhub.ai:5000/api/v1
ws_proxywss://prod.skyhub.ai:7070ws://localhost:7070wss://prod.skyhub.ai:7070wss://ws_proxy.skyhub-dev.internal:7070
janusGatewayUrlwss://prod.skyhub.ai:8188ws://localhost:8188wss://prod.skyhub.ai:8188wss://dev.skyhub.ai:8188
janusIceServers['stun:stun.l.google.com:19302'](same)(same)(same)
httpSessionExpiryTime8 (minutes)888
stripePublishableKeypk_live_xxx(absent)pk_live_xxx(absent)
assetsUrlhttps://skyhub-prod-assets.s3.eu-central-1.amazonaws.com/drone(same)(same)(same)
enableIsaacSimfalsefalsefalsefalse
The default environment.ts mixes a localhost REST URL with production ws_proxy, janusGatewayUrl, and stripePublishableKey. Running the default config against a local Gateway still points commands, video, and billing at prod.skyhub.ai. For fully-local work use environment.local.ts (npm start maps it via the local file-replacement — see Environments, Build & CI).

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
arm(droneId: string): Observable<any> {
  return this.http.post<any>(`${environment.url}/drone/action/arm`, {
    drone_id: droneId,
  });
}
The 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
startVideoStream(drone_id: number): Observable<any> {
  return this.http.get<any>(`${environment.url}/video_room/${drone_id}/start?update=true`);
}
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
// HTTP API uses:  http://localhost:5000/api/v1
// Socket.IO uses: http://localhost:5000 (root namespace)
const baseUrl = environment.url.replace('/api/v1', '');
const socket = io(baseUrl, {
  transports: ['websocket'],
  query: { token },              // JWT access token from localStorage
  reconnection: true,
  reconnectionDelay: 1000,
  reconnectionAttempts: 5,
});
The telemetry base URL is derived by string replacement of /api/v1. Any change to the REST path shape silently breaks the Socket.IO connection — there is no independent config key for the telemetry host.
The auth token is passed as the 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
const wsUrl = `${environment.ws_proxy}/redispad/${droneId}?access_token=${encodeURIComponent(accessToken)}`;
const socket = new WebSocket(wsUrl);
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.
ArucoOverlayService opens a second, independent WebSocket to the same /redispad/{droneId} URL (aruco-overlay.service.ts:89) to receive aruco_tracking frames for the precision-landing canvas overlay. So a selected physical drone typically holds two redispad sockets. This is intentional — do not merge them. (ArUco is skipped for SITL drones.)
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
server = environment.janusGatewayUrl;          // wss://…:8188
iceServers = environment.janusIceServers;      // ['stun:stun.l.google.com:19302']
roomId: number = 1234;                          // default; overridden by drone.video_room_id
pin: string = '';
The room lifecycle is REST-triggered (Channel 1’s 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 just localhost:{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. :5000gateway.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

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.
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.
VehicleCommandService and ArucoOverlayService each open their own WebSocket to /redispad/{droneId}. Expect two connections, not one.
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.
environment.ts combines a localhost REST URL with production ws_proxy/janus/Stripe values. Use environment.local.ts for a fully-local stack.
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.

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.