The SkyHub Gateway Service is the control-plane hub of the platform. It is a Python/Flask + Flask-SocketIO backend that authenticates users over HTTP (JWT), streams real-time telemetry to the Dashboard over Socket.IO, and dispatches flight commands over rosbridge WebSocket (port 9090) to either physical drones (reachable via a per-user WireGuard VPN) or SITL Docker containers it spawns on a remote Docker host. It is also the platform’s integration point for Stripe billing, AWS (S3/ECR/STS/EC2), and the Janus video SFU. Everything a client can do lives behind one contract: ~113 HTTP endpoints, all under the /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

ConcernTechnologyNotes
LanguagePython 3.10.12
Web frameworkFlask 3.0.2Blueprint-based routing
Real-timeFlask-SocketIO 5.3.6async_mode="gevent"; JWT handshake via ?token=
WSGI servergunicorn 22.0.0 + gevent-websocketGeventWebSocketWorker, 1 worker / 8 threads, binds main:app
AuthFlask-JWT-Extended 4.5.3, Flask-BcryptHS256 access (10 min) + refresh (12 hr) tokens
API docsflasgger 0.9.7.1Swagger 2.0 UI at /api/docs/, spec at /apispec.json
PersistenceFlask-SQLAlchemy + Flask-Migrate (Alembic)PostgreSQL
Cloudboto3 1.35.10S3, ECR, STS, SSM, EC2
Billingstripe SDKCheckout, portal, signature-verified webhooks
ObservabilityOpenTelemetry (OTLP)Traces/logs to SigNoz — see OpenTelemetry & SigNoz
The container entrypoint is defined in the Dockerfile:
Dockerfile
CMD ["gunicorn", "--workers", "1", "--threads", "8", \
     "-k", "geventwebsocket.gunicorn.workers.GeventWebSocketWorker", \
     "-b", "0.0.0.0:5000", "main:app"]
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 at GET /api/docs/ (raw spec at GET /apispec.json); both are public. Gateway Swagger UI (SkyHub Gateway Service API v1.0.0, 113 endpoints) 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 in src/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).
BlueprintFileEndpointsPrimary auth model
auth_routessrc/routes/auth_routes.py9Public (login/register) + JWT + activation token-in-path
drone_routessrc/routes/drone_routes.py30JWT; /drone/activate (10-digit header token), /drone/pull (VPN IP)
execution_routessrc/routes/execution_routes.py15Mixed: VPN-IP drone callbacks + JWT UI reads
geofence_routessrc/routes/geofence_routes.py11JWT
billing_routessrc/routes/billing_routes.py10JWT + public pricing + Stripe-Signature webhook
mission_routessrc/routes/mission_routes.py9JWT
asset_routessrc/routes/asset_routes.py8Mixed: VPN-IP upload callbacks + JWT reads
calendar_routessrc/routes/calendar_routes.py7JWT
vpn_routessrc/routes/vpn_routes.py5JWT
video_room_routessrc/routes/video_room_routes.py4JWT; create authed by drone video-room token
isaac_sim_routessrc/routes/isaac_sim_routes.py4JWT
user_routessrc/routes/user_routes.py1JWT
geofence_routes is registered and active even though the top-level CLAUDE.md architecture summary omits geofences. Trust src/main.py for the live blueprint list.

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 gamepad callbacks. The check_vpn_ip decorator (src/middleware/drone_vpn.py) trusts a request whose remote_addr / X-Real-IP / X-Forwarded-For starts with 10.71. (the drone WireGuard subnet), or an X-Drone-IP header naming a SKYHUB_SITL_* container when ENABLE_SITL is set. It sets request.vpn_ip and returns 400 otherwise. 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-Signature header on POST /billing/webhook.
Full details, status-code conventions, and the auth-per-endpoint matrix live in HTTP API Overview & Auth Models.

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 root src/application/app.py and imported by the routes that need it. A representative handler:
src/routes/user_routes.py
@user_routes.route("/user", methods=["GET"])
@jwt_required()
def get_user_info():
    try:
        user_id = get_jwt_identity()
        user = user_service.get_user_by_id(user_id)
        if not user:
            return get_error_response("User not found", 404)
        return get_success_response(user.to_dict())
    except Exception:
        return get_error_response("Cannot get user info", 500)

Response envelope

Newer routes use the helpers in src/utils/common_helper.py:
  • get_success_response(data, message, status_code){"success": true, "data": ..., "message": ...} (common_helper.py:4). Omit data to drop the key; pass data=None to include an explicit null.
  • get_error_response(code, message)({"success": false, "error": {"code": code, "message": message}}, code) (common_helper.py:24).
The envelope is not universal. Older drone_routes, mission_routes, and geofence_routes return raw jsonify(...) with bare fields and frequently use 201 for reads and updates (e.g. GET /drones, PATCH /mission/point/<id>). The current Dashboard depends on these exact shapes and codes — changing them is a breaking change. This inconsistency is catalogued in HTTP API Overview & Auth Models.

Where things live (src/ map)

PathRole
src/main.pyApp factory / gunicorn entrypoint: registers the 12 blueprints, Swagger, CORS, JWTManager, Bcrypt, SocketIO, and wires socket routes
src/application/app.pyComposition root — service singletons, the get_service(drone_type) factory, and get_vpn_service() lazy init
src/application/settings.pyEnvironment configuration (see Gateway Environment Variables)
src/routes/HTTP blueprints + socket_routes.py (Socket.IO telemetry handlers)
src/service/Business-logic tier (see Service Layer & Factory)
src/models/SQLAlchemy ORM models (User, Drone, Mission, Asset, MissionExecution, Subscription, Geofence, Calendar…)
src/rosbridge/Drone WebSocket connection management: connection.py, request_format.py, socket.py
src/middleware/drone_vpn.py — the check_vpn_ip decorator
src/utils/common_helper.py, drone_types.py, mavros_topics.py, drone_utils.py, email_templates.py
src/connector/db_connection.py — DB/SQLAlchemy wiring
src/diagnostic/logs.py — logging + OpenTelemetry log configuration
src/drone/messages.py — drone message helpers
src/migrator.pyAlembic migration helper (see Migrations & DB Connection)

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.