Route handlers are intentionally thin — they validate input, pick an auth model, and delegate to a service wired in
src/application/app.py. The business logic lives in the service layer, not the routes.Base path
Every HTTP endpoint lives under/api/v1. All 12 blueprints are registered with url_prefix="/api/v1/" in src/main.py:222-233, and each route rule inside a blueprint also starts with /, so the effective base is /api/v1 (the doubled slash collapses).
src/main.py:222-233
environment.url points at https://<host>/api/v1; requests land on the public WireGuard jumphost’s nginx on port 5000, which TLS-terminates and proxies to the Gateway container. See VPN IP Authentication & Jumphost Routing.
The 12 blueprints
| Blueprint | File | Endpoints | Primary auth |
|---|---|---|---|
auth_routes | src/routes/auth_routes.py | ~9 | Public + JWT (login, refresh, logout, register, activate, reset) |
drone_routes | src/routes/drone_routes.py | ~29 | JWT Bearer, plus /drone/activate (token header) and /drone/pull (VPN IP) |
execution_routes | src/routes/execution_routes.py | ~15 | Split: VPN-IP drone callbacks + JWT UI routes |
geofence_routes | src/routes/geofence_routes.py | ~11 | JWT Bearer |
isaac_sim_routes | src/routes/isaac_sim_routes.py | ~4 | JWT Bearer |
mission_routes | src/routes/mission_routes.py | ~9 | JWT Bearer |
video_room_routes | src/routes/video_room_routes.py | ~4 | JWT Bearer, except POST /video_room (video-room token) |
vpn_routes | src/routes/vpn_routes.py | ~5 | JWT Bearer |
asset_routes | src/routes/asset_routes.py | ~8 | JWT Bearer, except upload pair (VPN IP) |
user_routes | src/routes/user_routes.py | 1 | JWT Bearer |
billing_routes | src/routes/billing_routes.py | ~10 | JWT Bearer, plus /billing/pricing (public) and /billing/webhook (Stripe-Signature) |
calendar_routes | src/routes/calendar_routes.py | ~7 | JWT Bearer |
geofence_routes is registered (src/main.py:225) even though the top-level CLAUDE.md architecture summary omits geofences. Trust the blueprint list above.Response envelope
Newer routes (auth, executions, assets, billing, calendar, vpn, user) use two helpers fromsrc/utils/common_helper.py that return a (dict, status_code) tuple:
src/utils/common_helper.py:4-25
{"success": true, "data": {...}} and an error like {"success": false, "error": {"code": 404, "message": "..."}} with the HTTP status equal to code.
Older routes return raw jsonify
The drone, mission, and geofence blueprints predate the envelope and return bare fields via flask.jsonify, frequently with non-standard status codes:
Which routes use which shape?
Which routes use which shape?
- Envelope (
{success, data|error}):auth(partially),execution,asset,billing,calendar,vpn,user. - Raw
jsonifybare fields:drone,mission,geofence, and the olderauthhandlers (login/refresh/register). - When adding an endpoint, prefer
get_success_response/get_error_responsefor consistency with the newer surface.
Import path gotcha
Import path gotcha
Most routes import the helpers from
src.utils.common_helper, but src/routes/asset_routes.py imports them from utils.common_helper (no src. prefix). Both module paths must stay importable — don’t remove either.Pagination
List endpoints that acceptlimit/offset (GET /executions and GET /executions/vehicle/<id>) clamp the parameters with clamp_pagination — max limit 100, offset floored at 0:
src/utils/common_helper.py:28-39
limit=5000 therefore returns at most 100 rows; there is no cursor pagination.
GET /billing/payments is different: it accepts only limit (default 20, no offset) and does not use clamp_pagination — a limit outside 1..100 returns HTTP 400 ("Limit must be between 1 and 100") rather than being clamped.
The three auth models
Three distinct authentication models coexist. Every endpoint uses exactly one — pick the right one when adding a route. Deep behavior lives in Authentication & JWT Lifecycle and Authentication & Security Model.JWT Bearer
The UI.
Authorization: Bearer <jwt>, HS256, signed with JWT_SECRET_KEY. Access token 10 min, refresh 12 hr (src/main.py:247-248). Guards nearly all UI endpoints via @jwt_required().VPN source IP
Drone & gamepad callbacks. The
check_vpn_ip decorator (src/middleware/drone_vpn.py) trusts a source IP starting with 10.71. (the WireGuard drone plane) — no JWT.Token / signature
Bootstrap & third parties: 10-digit drone activation token header, video-room Bearer token, and the Stripe-Signature header on the billing webhook.
1. JWT Bearer (the UI)
The Dashboard logs in viaPOST /api/v1/login, receives an access + refresh token, and attaches Authorization: Bearer <access> to every subsequent request. Handlers use @jwt_required() and read the caller via get_jwt_identity().
2. VPN source IP (check_vpn_ip)
Drone- and gamepad-originated callbacks carry no JWT. The check_vpn_ip decorator authenticates purely by source IP — it accepts remote_addr, X-Real-IP, or X-Forwarded-For beginning with 10.71. (the drone WireGuard CIDR), sets request.vpn_ip, and returns 400 otherwise. In local/SITL mode it additionally honors an X-Drone-IP header (SITL container names when ENABLE_SITL, or safe local prefixes such as 127., 172.17., 192.168., 10.223.).
src/middleware/drone_vpn.py:40-47
drone_service.get_drone_by_ip(request.vpn_ip). Endpoints guarded this way: GET /drone/pull, POST /executions/start|<id>/complete|<id>/log, GET /executions/current, and POST /authenticate_upload|/complete_upload.
3. Token / signature auth
Three endpoints authenticate with a shared secret rather than a user identity:| Endpoint | Credential | Notes |
|---|---|---|
GET /api/v1/drone/activate | 10-digit token request header | Physical-drone bootstrap; returns ECR creds + presigned compose + VPN link, then clears the token. 406 on bad token. |
POST /api/v1/video_room | Authorization: Bearer <video_room_token> | Matched via get_drones_by_video_room_id_and_token; not a user JWT. Returns 402 when the token is missing. |
POST /api/v1/billing/webhook | Stripe-Signature header | Verified against STRIPE_WEBHOOK_SECRET; verification is skipped only in local env. 400 on invalid signature. |
POST /video_room returning 402 Payment Required for a missing token is an unusual choice — it is an auth failure, not a billing one. Preserved for client compatibility.Interactive API docs (Swagger)
A Swagger 2.0 spec is generated from route docstrings by flasgger. The UI is served at/api/docs/ and the raw spec at /apispec.json (both public), configured in src/main.py:24-77 with basePath: /api/v1 and a Bearer security definition.
Conventions to preserve
Status codes are load-bearing
Status codes are load-bearing
Older routes use
201 for reads/updates and 402 for a video-room auth error. The Dashboard matches on these exact codes. Normalize only alongside a coordinated frontend change.Two envelopes, on purpose
Two envelopes, on purpose
Don’t mass-migrate raw-
jsonify routes to the envelope without checking the consuming Angular services — response-shape changes are breaking. New endpoints should use the envelope.Duplicate arm endpoint
Duplicate arm endpoint
POST /drone/action/arm and POST /drone/action/arm_drone are functionally identical (both arm and pre-push the SITL video room). Disarm exists only as POST /drone/action/disarm_drone. See Drone Management & Control Actions.Async handlers
Async handlers
A few handlers are Flask
async def (create_drone, delete_drone, create_video_room, restart_video_stream) and depend on the gevent worker. Keep the async signature when editing them.See also
Authentication & JWT Lifecycle
Login/refresh/logout/activate flows, token expiry, the in-memory blocklist, and the activation
level claim.Socket.IO Telemetry Streaming
The real-time channel:
?token= handshake, room naming, and stream types.VPN Middleware & Jumphost
How source-IP trust and jumphost routing actually work in production.
Full HTTP & Socket.IO Reference
The complete, per-endpoint API reference across the platform.

