These two blueprints are the persistence layer for flight plans. They store waypoints and fence vertices in PostgreSQL in a MAVLink-compatible shape, but they do not talk to a drone — uploading a stored mission/fence to a vehicle is a separate /drone/action/* call documented in Drone Management & Control Actions. Think of this page as “the database side of the map editor.”
  • src/routes/mission_routes.py — mission + mission-point CRUD (9 endpoints), backed by MissionService (src/service/mission_service.py) and PointService (src/service/point_service.py).
  • src/routes/geofence_routes.py — geofence + geofence-point CRUD/toggle/batch (11 endpoints), backed by GeofenceService (src/service/geofence_service.py).
Every endpoint on this page is @jwt_required() (JWT Bearer, the UI auth model in HTTP API Overview). Unlike Socket.IO telemetry rooms, these routes are genuinely user-scoped: every service query filters user_id == get_jwt_identity(), so a token can only see and mutate its own missions and geofences.

Lifecycle at a glance

The stored rows are MAVLink-shaped so the upload path (push_mission, push_geofence, sync_geofences in src/service/drone_control_service.py) can convert each row to a rosbridge waypoint with almost no transformation. The exact conversion and the on-drone protocol live in Mission & Geofence MAVLink Format.
The CLAUDE.md “Mission Format” note claims the backend auto-generates a reversed return path plus a trailing RTL (command 20) and a speed row (command 178). That is stale. push_mission (src/service/drone_control_service.py:784) uploads your stored waypoints as-is and only prepends a synthetic TAKEOFF (command 22) when the first waypoint isn’t already a takeoff. Nothing reverses the path or appends RTL. Do not reintroduce that behavior when editing.

Missions API

All routes are under the /api/v1 base path.
Method & PathPurposeSuccessErrors
POST /missionCreate an (empty) mission201500
POST /mission/pointAdd one mission point (array body, uses [0])201500
PATCH /mission/point/<mission_point_id>Update one point201404, 500
GET /mission/point/<mission_point_id>Get one point200404, 500
GET /missionsList all missions with points200500¹
GET /mission/<mission_id>Get one mission with points200404, 500
DELETE /mission/<mission_id>Delete mission (+ its points)200400², 500
DELETE /mission/point/<mission_point_id>Delete one point201500
PATCH /mission/<mission_id>/pointsBatch update points201500
¹ The GET /missions error branch returns jsonify(error=...) with no status code, so a failure serializes the error body with an HTTP 200 (src/routes/mission_routes.py:323) — a latent bug to preserve/fix deliberately. ² 400 only when the DB raises a foreign-key violation (drone_mission_id_fkey).
Mission point mutations return 201 for create and update and delete and batch. That is not a typo in this doc — it is what the handlers actually return. On the geofence side only point create returns 201 (same as mission); point update, delete, and batch return 200. Any client (and any refactor) must treat both as success; do not “normalize” these codes without updating the Dashboard.

Create a mission, then add points

1

Create the mission shell

POST /mission takes just a name and returns the new id. MissionService.create_mission enforces a UNIQUE(user_id, name) constraint (src/models/mission.py:19).
curl -X POST https://<gateway-host>/api/v1/mission \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Survey Mission 1"}'
# 201 -> {"id": 42, "name": "Survey Mission 1", "mission_points": []}
2

Add points one at a time

POST /mission/point expects a JSON array, but the handler only reads element [0] (src/routes/mission_routes.py:138). Sending several points in one call silently persists only the first.
curl -X POST https://<gateway-host>/api/v1/mission/point \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[{"mission_id": 42, "lat": 42.6977, "lng": 23.3219,
        "altitude": 30, "type": "fly", "sequence": 0}]'
The response is the whole parent mission (id, name, and the full mission_points array), not just the created point.

Mission point fields

create_mission_point (src/service/mission_service.py:106) reads the keys below. The underlying mission_point table columns and their nullability come from src/models/mission_point.py.
FieldRequiredDefaultNotes
mission_idyesFK → mission.id (ON DELETE CASCADE)
latyesNOT NULL
lngyesNOT NULL
altitudeyesmeters; NOT NULL
typeyesenum base | fly | safepoint; NOT NULL
sequenceyesinteger ordering key; NOT NULL
labelnonullfree text
frameno30 = GLOBAL, 3 = GLOBAL_RELATIVE_ALT
commandno1616 = WAYPOINT, 22 = TAKEOFF, 21 = LAND, 20 = RTL
is_currentnofalse
autocontinuenotrue
param1param4no0.0MAVLink command params
The Swagger block for POST /mission/point documents an order property, but the handler and service actually read sequence — there is no order mapping. Because sequence is NOT NULL, sending order instead of sequence (or omitting type/altitude) hits a DB NOT NULL/enum violation and surfaces as a bare 500. Trust the code (sequence), not the Swagger.

Batch update (reorder / drag-to-edit)

PATCH /mission/<mission_id>/points is what the editor calls after a drag-reorder. The body is an array of {id, ...} objects and PointService.batch_update (src/service/point_service.py:47) passes each raw dict straight into MissionPoint.query...update(point_data).
curl -X PATCH https://<gateway-host>/api/v1/mission/42/points \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[{"id": 101, "sequence": 0, "lat": 42.70, "lng": 23.32, "altitude": 30},
       {"id": 102, "sequence": 1, "lat": 42.71, "lng": 23.33, "altitude": 40}]'
Because the raw request dict is handed to SQLAlchemy’s .update(), every key must be a real mission_point column (id, lat, lng, altitude, type, label, sequence, frame, command, param1param4). An unknown key such as order raises and the whole batch fails with 500. Each row is also re-scoped by user_id, so you can only touch your own points.

Deleting

DELETE /mission/<mission_id> (MissionService.delete, src/service/mission_service.py:83) does three things in order: sets Drone.mission_id = NULL on any drone pointing at this mission, deletes the mission’s points, then deletes the mission. The route still wraps a foreign-key violation into a friendly 400 (“unassign it first”) in case a drone reference survives.

Geofences API

Geofences model MAVLink fence zones — polygons and circles, either inclusion (stay inside) or exclusion (stay outside).
Method & PathPurposeSuccessErrors
POST /geofenceCreate a geofence201500
POST /geofence/pointAdd one vertex/center (array body, uses [0])201500
PATCH /geofence/point/<point_id>Update one point200404, 500
GET /geofence/point/<point_id>Get one point200404, 500
GET /geofencesList geofences with points200500
GET /geofence/<geofence_id>Get one geofence with points200404, 500
PATCH /geofence/<geofence_id>Update metadata (name/type/fence_type/enabled)200404, 500
PATCH /geofence/<geofence_id>/toggleFlip the enabled flag200404, 500
DELETE /geofence/<geofence_id>Delete geofence (points cascade)200404, 500
DELETE /geofence/point/<point_id>Delete one point200404, 500
PATCH /geofence/<geofence_id>/pointsBatch update points200500
PATCH /geofence/<id>/toggle exists so the “enable this fence” checkbox does not need to round-trip the whole object. sync_geofences on the drone side only uploads geofences whose enabled is true, so the toggle is the switch that decides whether a fence actually reaches the aircraft. See Drone Management & Control Actions.

Geofence metadata fields

Set on POST /geofence and PATCH /geofence/<id> (src/service/geofence_service.py:78; columns in src/models/geofence.py):
FieldRequiredDefaultValues
nameyesunique per user (UNIQUE(user_id, name))
typeyespolygon | circle
fence_typenoexclusioninclusion | exclusion
enablednotrueboolean

Geofence point fields

create_geofence_point (src/service/geofence_service.py:94) reads:
FieldRequiredDefaultNotes
geofence_idyesFK → geofence.id (ON DELETE CASCADE)
latyesNOT NULL
lngyesNOT NULL
sequenceyesordering key; NOT NULL
commandyesnonesee warning below
frameno3GLOBAL_RELATIVE_ALT
param1param4no0.0for polygons, param1 is the vertex count; for circles it is the radius (m)
command is NOT NULL in the geofence_point table but the service supplies no default (command=point_details.get("command")), so omitting it 500s. In practice the exact value you store barely matters: on upload, push_geofence / sync_geofences overwrite each point’s command with get_fence_command(type, fence_type) and recompute param1 (src/service/drone_control_service.py:892, :1009). Supply a placeholder (e.g. 5002) to persist; the real command is derived from the parent geofence’s type + fence_type at push time.

Batch update

PATCH /geofence/<id>/points mirrors the mission batch endpoint — GeofenceService.batch_update_points (src/service/geofence_service.py:192) passes each raw dict into .update(), so keys must be real geofence_point columns (id, lat, lng, sequence, frame, command, param1param4) and rows are re-scoped by user_id. These are the values the fields above map onto. The upload layer converts each row via to_mavlink_waypoint() / to_mavlink_fence_item(), renaming lat → x_lat, lng → y_long, altitude → z_alt (fence points force z_alt = 0.0, they are 2D).

Mission commands (command)

  • 16NAV_WAYPOINT
  • 22NAV_TAKEOFF (auto-prepended if missing)
  • 21NAV_LAND
  • 20NAV_RETURN_TO_LAUNCH
Frames: 0 = GLOBAL, 3 = GLOBAL_RELATIVE_ALT.

Fence commands (command)

  • 5001POLYGON_VERTEX_INCLUSION
  • 5002POLYGON_VERTEX_EXCLUSION
  • 5003CIRCLE_INCLUSION (param1 = radius)
  • 5004CIRCLE_EXCLUSION (param1 = radius)
Derived by get_fence_command() in src/models/geofence_point.py.

Gotchas worth preserving

POST /mission/point and POST /geofence/point both accept a JSON array but process only data[0] (mission_routes.py:138, geofence_routes.py:154). The batch PATCH .../points endpoints are the real multi-point path. If a future editor wants to add many points in one request, it must loop the single-create endpoint or extend the handler — the array shape is a historical leftover, not multi-insert support.
Adding, updating, or deleting a single point returns the entire mission/geofence object with its refreshed *_points array — not the point you touched. This lets the editor replace its local state in one shot, but means the response can be large for big plans.
Unlike the newer blueprints (auth, executions, billing), mission/geofence routes return bare jsonify payloads (the object itself, or {"error": "..."} / {"message": "..."}), not the {"success": true, "data": ...} envelope from src/utils/common_helper.py. Clients must not assume a uniform envelope across the gateway. See HTTP API Overview.
Mission point create/update/delete/batch all return 201. On the geofence side only point create returns 201 (same as mission); point update/delete/batch return 200. GET /missions even returns 200 on error. These are load-bearing for the current Dashboard — treat any code < 400 as success and change codes only alongside the frontend.

Drone Management & Control Actions

push_mission, push_geofence, clear_fence, sync_geofences, start_mission — the endpoints that actually send a stored plan to a vehicle.

Mission & Geofence MAVLink Format

Full DB → MAVLink conversion, the TAKEOFF auto-prepend, and the on-drone fence/mission protocol.

HTTP API Overview & Auth Models

Base path, the three auth models, and the response-envelope conventions.

Mission & Waypoint Planning (Dashboard)

How the Angular editor builds these payloads and drives the batch-reorder calls.