The big picture
The Gateway Service is the control-plane hub. It is the only component that authenticates users, spawns SITL, and opens rosbridge WebSockets to drones, and it owns the primary read/write database access. Almost every arrow either starts or ends at the Gateway. Video and manual gamepad control are the two deliberate exceptions — they travel out-of-band so a slow API call can never stall a live feed or a joystick.
Control plane
Dashboard → Gateway over HTTP (JWT) + Socket.IO; Gateway → drone over rosbridge WebSocket on port 9090. Carries commands (arm, takeoff, set_mode, mission push) and all telemetry.
Video plane
On-drone GStreamer publishes H264 to the WHIP server, which registers a publisher in a Janus VideoRoom; the Dashboard subscribes over WebRTC. Fully out-of-band from the API.
Network plane
Per-user WireGuard tunnels make physical drones (10.71.x) reachable, with iptables isolation so users only reach the drones they own. One EC2 jumphost is the sole public ingress.
Manual-control plane
Gamepad frames travel Dashboard → WS Proxy → Redis pub/sub → drone at 60 Hz — a separate low-latency path that never touches the Gateway’s rosbridge channel.
The Gateway is the control-plane hub
Every operator action funnels through the Gateway Service. It:- Authenticates users over HTTP with JWT (10-min access, 12-hr refresh tokens).
- Owns the database —
User,Drone,Mission,Asset,UserDroneAccessall live in one PostgreSQL instance the Gateway reads and writes. - Dispatches drone commands by translating REST calls (
POST /api/v1/drone/action/{arm|takeoff|set_mode|push_mission|...}) into rosbridgecall_service/publishframes on a pooled, auto-reconnecting WebSocket (src/rosbridge/connection.py,src/service/drone_control_service.py). - Streams telemetry back to the browser over Socket.IO — the drone’s MAVROS topics arrive on the rosbridge connection and are re-emitted as
telemetry_dataevents. - Spawns and tears down SITL Docker containers on a remote Docker host (
src/service/sitl_drone_service.py), gated byENABLE_SITL. - Creates Janus video rooms and pushes the room credentials to the drone so it knows where to publish.
SITL and physical drones are interchangeable
A design keystone: the Gateway speaks the same rosbridge/topic contract to a real drone and to a SITL container. A SITL container runssim_vehicle.py + MAVROS + rosbridge_server + a GStreamer→WHIP video node — the identical interface a SkyCore drone exposes. The only differences the Gateway cares about are where to connect and lifecycle:
| Aspect | Physical drone | SITL container |
|---|---|---|
| rosbridge target | drone.ip (a 10.71.x WireGuard address) | DOCKER_HOST_IP / office server over WireGuard |
| Reachability | Per-user WireGuard tunnel + iptables | Docker host on the infra VPN (10.69.x) |
| Lifecycle | Boots, calls GET /drone/activate | Gateway creates/destroys via the Docker API |
| Everything else | rosbridge :9090, MAVROS topics, WHIP video | Identical |
Three real-time transport channels
Do not conflate these — they are independent connections with different protocols and different clients. Tuning or debugging one has no effect on the others.Socket.IO (telemetry)
Browser ↔ Gateway. JWT in the
?token= query param. Rooms are named drone_{id}_dashboard. Carries GPS, altitude, logs, diagnostics.rosbridge WebSocket
Gateway ↔ drone on port 9090. A persistent, auto-reconnecting, subscription-replaying connection per drone, pooled in
DroneControlService.WebRTC + WS Proxy
Video rides WebRTC through Janus; gamepad rides a raw WebSocket to the WS Proxy and Redis pub/sub. Both bypass the Gateway.
TELEMETRY_THROTTLE_RATE, default 200 ms), reconnect/backoff, and the room-naming details for each.
Three auth models coexist
Different actors authenticate different ways. A change to one must not accidentally widen another.| Model | Used by | Mechanism |
|---|---|---|
| JWT Bearer | Dashboard / operators | HS256 access + refresh tokens; Authorization: Bearer, or ?token= for the Socket.IO handshake |
| VPN source-IP trust | Drone / gamepad callbacks | check_vpn_ip (src/middleware/drone_vpn.py) trusts any source in the 10.71.x drone subnet or a whitelisted X-Drone-IP. No cryptographic device identity — network trust only |
| Token / signature | Activation, video rooms, Stripe | 10-digit activation header token, per-room video token, and Stripe-Signature webhook verification |
/drone/pull, /executions/*, /authenticate_upload) carry no JWT. See Authentication & Security Model and the VPN Middleware & Jumphost page.
The jumphost: keystone and single point of failure
The jumphost’s nginx reverse proxy fronts these ports (all on the WireGuard EC2 host):| Port | Routes to | Purpose |
|---|---|---|
443 / 80 | S3 UI bucket | Dashboard SPA |
5000 | gateway.skyhub-prod.internal:5000 | REST API + Socket.IO (/socket.io/) |
7070 | ws_proxy.skyhub-prod.internal:7070 | Gamepad WebSocket |
8188 | janus.skyhub-prod.internal:8188 | Janus WebRTC signalling |
9090 | http://$http_x_drone_ip:$http_x_drone_port | Dynamic rosbridge proxy via x-drone-ip / x-drone-port headers |
2375 | office server <office-docker-host>:2375 | Remote Docker API for SITL |
4317 | office server <office-docker-host>:4318 | OTLP traces/logs → SigNoz |
UDP 20000-21100 | Janus | WebRTC media relay |
:9090 header-driven proxy is what lets the Gateway reach any drone with a single connection string — it sets x-drone-ip/x-drone-port (src/rosbridge/connection.py:162) and nginx forwards accordingly. See Network & VPN Topology and VPC, WireGuard Jumphost & nginx Routing for the full port map and the WireGuard address plan.
Shared infrastructure
Three data stores are touched by multiple services, so schema or availability changes ripple widely:- PostgreSQL — the single
skyhubdatabase (user,drone,user_drone_access, missions, assets, executions). Read/written by the Gateway, the WS Proxy, and the User VPN service. In production it runs as an ECS Fargate container on EFS — not RDS. - Redis — the pub/sub bus for the gamepad/manual-control path (
{drone_ip}:gamepad_input,{ip}:output,{ip}:aruco_tracking) and an optional Socket.IO message queue for multi-worker scaling. A container, not ElastiCache. - S3 —
skyhub-prod-assets(video/image/log assets),skyhub-prod-user-vpn(per-user WireGuard configs), and the UI bucket.
eu-central-1 on ECS Fargate behind Cloud Map service discovery (skyhub-prod.internal). See the AWS Infrastructure Overview for the full deployment.
Where to go next
Cross-System Data Flows
Step-by-step traces: user command, telemetry stream, video pipeline, SITL spawn, mission upload, gamepad control, and drone activation.
Real-time Transport Channels
The Socket.IO, rosbridge, and WebRTC/WS-Proxy channels in depth — heartbeats, throttling, reconnect, and rooms.
Authentication & Security Model
The three auth models, where each applies, and the trust boundaries a change must respect.
Network & VPN Topology
The three WireGuard planes, the jumphost, per-user drone isolation, and how a drone becomes reachable.

