Almost every environment variable the gateway reads flows through a single module — src/application/settings.py — which evaluates ~80 os.getenv() calls at import time, coerces types, and applies defaults. Routes, services, and the rosbridge layer all import these module-level constants, so the exact names, defaults, and coercion in settings.py are load-bearing: renaming or re-typing one is a breaking change across the whole codebase. A handful of variables bypass settings.py and are read directly with os.getenv() elsewhere (email, APP_ENVIRONMENT, AWS credentials), and a few values that look configurable are actually hardcoded. This page enumerates everything, groups it by concern, and calls out every place a default disagrees between settings.py, .env.example, and the exported production ECS task definition.
Deeper mechanics live on sibling pages — this page is the reference table. See Startup, Validation & Composition Root for boot ordering and the fail-fast guards, VPN IP Authentication & Jumphost Routing for the network-trust model, OpenTelemetry & SigNoz for the OTEL wiring, and Migrations, DB Connection & Dev Mode for the database URI and APP_ENVIRONMENT.

Where configuration comes from

settings.py raises RuntimeError at import time if JWT_SECRET_KEY is empty on any non-local deployment (src/application/settings.py:299) — this fires before validate_critical_config() runs, so anything that imports settings triggers it. Set DEPLOYMENT_ENVIRONMENT=local to fall back to an insecure test key, or provide a real key.

Default mismatches — read before deploying

settings.py defaults are what runs when a variable is unset. The .env.example template and the production ECS task definition (docs/aws_prod_env_vars.env) both override several of them, so the code default is frequently not what runs in production. Do not assume the settings.py value.
OTLP endpoint footgun. docker-compose.yml defaults the base endpoint to http://<office-docker-host>:4318, and the exporters append /v1/traces and /v1/logs themselves. But .env.example bakes the signal path into the base var (.../v1/traces), which would make the log exporter POST to /v1/traces/v1/logs. Production points at port 4317 (the gRPC port) while the code uses the HTTP exporter. Keep this a bare host:4318 base URL. See OpenTelemetry & SigNoz.

Deployment & environment

Database

The URI is assembled in DBConnector as postgresql://{DB_USERNAME}:{DB_PASSWORD}@{DB_IP}/{DB_NAME} (src/connector/db_connection.py:29). All four values are .strip()ped in settings.py.

Authentication & JWT

JWT_ACCESS_TOKEN_EXPIRES (10 min) and JWT_REFRESH_TOKEN_EXPIRES (12 h) are hardcoded timedeltas in main.py:247-248, not env-configurable — despite older docs listing them as env vars. The JWT algorithm is fixed to HS256, and the revocation BLOCKLIST is an in-memory set (does not survive restart or scale across workers).

VPN & jumphost routing

Jumphost mode is selected purely by whether JUMPHOST_IP is a non-empty string. Both the WebSocket Connection and its HTTP _test_connection() reachability probe must send identical x-drone-ip / x-drone-port headers, or the probe and the socket can diverge. Details in VPN Middleware & Jumphost.

SITL orchestration

SITL container lifecycle is documented in SITL Drone Lifecycle. Note the env-var-name → settings.py-constant remaps flagged inline below.
Applied to each new SITL drone at creation time. The battery values model the aircraft the simulated fleet stands in for — a T-Drones M690 Pro on a 6S 27 Ah pack.The three voltage failsafes are off deliberately: they fired off a different number than the supervisors reason with, so the autopilot RTL’d an aircraft the relay still considered to have margin. The charge-remaining thresholds (SITL_BATT_LOW_PCT 25, SITL_BATT_CRT_PCT 12) drive BATT_LOW_MAH / BATT_CRT_MAH instead.

Docker host

Video streaming

Video-room mechanics live in Janus Video Rooms. (SITL_VIDEO_STREAM_DRONE_STATE is listed under SITL above.)

Socket.IO

Streaming behavior is documented in Socket.IO Telemetry Streaming.

Rosbridge & connection

Connection/reconnect internals live in Rosbridge Connection & Reconnect.

Observability (OpenTelemetry / SigNoz)

Billing (Stripe)

Billing logic lives in Stripe Billing & Vehicle Limits.

Email (SMTP)

All four MAIL_* values are read directly with os.getenv() in src/service/email_service.py:13-16 — they are not surfaced in settings.py, so they are easy to miss when auditing config.

AWS & S3

REGION is the value settings.py exposes and is validated at startup; boto3 clients additionally pick up the standard AWS_* credential vars from the environment implicitly (they are never read in code).

Variables that are hardcoded, not env-driven

Some behavior that operators expect to configure via env is fixed in code. Changing it requires a code change, not a variable:
Adding a new frontend origin is a code change (main.py), not an env var. The two environment switches — DEPLOYMENT_ENVIRONMENT (local/server) and APP_ENVIRONMENT (dev/production) — are independent and easy to confuse: the first drives CORS/JWT/middleware behavior, the second only gates db.create_all() in the python main.py dev path.

Minimal local .env

The shipped .env.example boots a fully local dev stack (SITL enabled, insecure defaults). The smallest set that gets the gateway running against a local Postgres:
.env
For production parity, cross-reference Production Configuration and the platform-wide Environment Variables reference.

Startup, Validation & Composition Root

How these variables are validated and consumed during boot — fail-fast guards, init order, and dev-mode schema creation.