/api/v1 prefix, plus a single Socket.IO namespace for telemetry. This page is the map of that surface — what the gateway does, how it is structured, and where new logic belongs. Deep-dives on each area live in the sibling pages linked throughout.
What the gateway is responsible for
Auth & accounts
Login/refresh/register/activate, JWT issuance, users, VPN config, and Stripe billing.
Drone control
Drone CRUD plus ~20
/drone/action/* MAVROS commands (arm, takeoff, modes, params, missions, geofences).Real-time telemetry
Socket.IO rooms fed by pooled rosbridge connections, with server-side dashboard yaw computation.
SITL orchestration
Spawns and tears down the 3-container SITL stack over local or remote Docker.
Media & flight data
Janus video rooms, S3 assets/HLS, execution tracking, log analysis, archives, and email reports.
Integrations
Stripe webhooks, AWS ECR/STS bootstrap for drone self-update, and Isaac Sim EC2 control.
Tech stack
The container entrypoint is defined in the
Dockerfile:
Dockerfile
The default deployment runs a single gunicorn worker. Two pieces of state are process-local as a result: the JWT logout
BLOCKLIST (an in-memory set() in src/routes/auth_routes.py) and, if you scale to multiple workers, Socket.IO room fan-out — which is why SOCKETIO_MESSAGE_QUEUE (an optional Redis URL) exists. See Authentication & JWT Lifecycle.The API surface
The gateway exposes a Swagger 2.0 spec generated by flasgger. Browse it live atGET /api/docs/ (raw spec at GET /apispec.json); both are public.
Every route is registered under url_prefix="/api/v1/" in src/main.py:222-233, and each rule itself begins with /, so the effective base path is /api/v1. The Socket.IO handlers are wired separately (src/main.py:240) and are not a blueprint.
12 blueprints
Blueprints are registered insrc/main.py; each imports its collaborating service singleton from src/application/app.py and stays thin. Endpoint counts below are the @*_routes.route(...) decorators in each file (113 total).
Three coexisting auth models
The gateway serves three different callers, each trusted by a different mechanism. This is intentional — the same rosbridge/topic contract makes SITL and physical drones interchangeable, but the callers are not.- JWT Bearer — the Dashboard. Validated by Flask-JWT-Extended (HS256). See Authentication & JWT Lifecycle.
- VPN source IP — physical drone and agent callbacks. The
check_vpn_ipdecorator (src/middleware/drone_vpn.py) trusts a request whoseremote_addr/X-Real-IP/X-Forwarded-Forstarts with10.71.(the drone WireGuard subnet), or anX-Drone-IPheader naming aSKYHUB_SITL_*container whenENABLE_SITLis set. It setsrequest.vpn_ipand returns400otherwise. These endpoints carry no JWT. See VPN IP Authentication & Jumphost Routing. - Token / signature — the 10-digit drone activation token header, the per-drone video-room bearer token, and the
Stripe-Signatureheader onPOST /billing/webhook.
Design: thin routes, service delegation
Route handlers do the boring parts — parse the request, authorize, and translate a service result into an HTTP response — then delegate all business logic to a service singleton. Every service is instantiated once at import time in the composition rootsrc/application/app.py and imported by the routes that need it. A representative handler:
src/routes/user_routes.py
Response envelope
Newer routes use the helpers insrc/utils/common_helper.py:
get_success_response(data, message, status_code)→{"success": true, "data": ..., "message": ...}(common_helper.py:4). Omitdatato drop the key; passdata=Noneto include an explicit null.get_error_response(code, message)→({"success": false, "error": {"code": code, "message": message}}, code)(common_helper.py:24).
Where things live (src/ map)
Deciding where new backend logic belongs
1
Route handler — request plumbing only
Parse and validate input, run the auth decorator (
@jwt_required() or @check_vpn_ip), call one service, and shape the response with get_success_response / get_error_response. Keep it thin.2
Service — the business logic
Add or extend a class in
src/service/. Drone operations should go through DroneControlService (rosbridge dispatch) or a DroneService subclass obtained via get_service(drone_type). See Service Layer & Factory and DroneControlService & Rosbridge Dispatch.3
Wire the singleton
Instantiate any new service once in
src/application/app.py and import it from the route module — do not construct services inside request handlers.4
Persistence & external calls
Add ORM models under
src/models/; reach drones only through the pooled rosbridge Connection; put S3/Stripe/Janus/EC2 access behind a service, never in a route.Explore the gateway
HTTP API Overview & Auth Models
Base path, the 12 blueprints, response envelope, and the three auth models in depth.
Drone Management & Control Actions
Drone CRUD and the ~20
/drone/action/* MAVROS commands.Socket.IO Telemetry Streaming
Handshake, room subscriptions, stream types, and the dashboard fan-out.
Executions, Assets & Reports
VPN-IP drone callbacks, upload flow, log analysis, archives, and reports.
Missions & Geofences API
Mission and geofence CRUD plus MAVLink waypoint format.
Billing, Calendar, VPN, Video & Isaac Sim
Stripe endpoints, scheduling, VPN config, video rooms, and Isaac Sim control.
Service Layer & get_service Factory
How services are wired as singletons and the physical/SITL factory.
Rosbridge Connection & Reconnect
The persistent drone WebSocket, reconnect, and subscription tracking.

