SkyHub configuration lives in four independent surfaces, each read by a different service at a different moment in its lifecycle. This page is the cross-service index: it names every variable, its default, and what it controls, then points you at the per-service deep-dive pages. Use it to answer “which var controls X?” and “why does dev behave differently from prod?” — the answer is almost always a default that differs between a Python os.getenv() fallback and the .env / tfvars template that ships alongside it.
For the detailed treatment of a surface — startup validation order, footguns, and the exact code path that reads each value — follow the per-service links: Gateway env vars · Dashboard build & config · Drone OS services · AWS environments & Terraform. This page is the flat, copy-pasteable index across all of them.

The four config surfaces

SurfaceSource of truthFormatRead whenDetail page
Gateway Servicesrc/application/settings.py (defaults) + .env.example / docker-compose.yml / SSMshell envContainer start (import time)Gateway env vars
Dashboardsrc/environments/environment*.tsTypeScript objectng build (compile time)Dashboard build & config
SkyCore Drone OS.env (template .env.example)shell envOn-drone docker compose upDrone OS services
AWS / Terraformskyhub_terraform/environments/<env>/settings.tfvarsHCLterraform applyEnvironments & state
Gateway config is evaluated at import time. settings.py raises RuntimeError the moment it is imported if JWT_SECRET_KEY is empty on a non-local deployment — before any validation function runs. Dashboard config, by contrast, is frozen into the JavaScript bundle at build time: changing environment.prod.ts requires a rebuild and redeploy, not a restart. See Startup & validation.

Default mismatches you must reconcile

The single most common source of “works in dev, breaks in prod” is a Python fallback in settings.py that disagrees with the value shipped in the env template or baked into the production task definition. These are the known divergences:
Variablesettings.py fallback.env.example / prodWhy it matters
DEPLOYMENT_ENVIRONMENTserver.env.example: localFlips CORS, JWT default, SITL, and IP-trust behavior
USER_SITL_MAX_COUNT3.env.example / prod: 5Max SITL drones per user
SEND_QUEUE_SIZE30prod: 200Per-connection outgoing rosbridge queue depth
SITL_VIDEO_STREAM_DRONE_STATEARMED.env.example / prod: CONNECTEDDrone state at which SITL video starts
LOG_LEVEL20 (INFO)prod: 10 (DEBUG)Root logger verbosity
Never assume the settings.py fallback is what runs in production. Treat the deployed env (docker-compose defaults for local, the ECS task definition / SSM for prod) as authoritative and the Python fallback as a last-resort safety net.

Gateway Service (skyhub_gateway_service)

Every constant below is defined in src/application/settings.py unless noted. Defaults shown are the Python os.getenv() fallback. Secrets are redacted — supply real values via .env (local) or SSM (prod).
VariableDefaultPurpose
DEPLOYMENT_ENVIRONMENTserverMaster switch. local sets IS_LOCAL_ENVIRONMENT=true (permissive CORS, insecure JWT fallback, X-Drone-IP local trust, dev SITL). Any other value = strict/non-local.
ENABLE_SITLtrueGates lazy SITLDroneService init and SKYHUB_SITL_* X-Drone-IP trust in middleware.
ENABLE_REGISTRATIONtrueToggles public user registration (auth_routes.py).
ADMINS_ONLYfalseDefined but referenced nowhere in src/ — dead config.
APP_ENVIRONMENTproductionRead only in main.py __main__ (not in settings.py). dev runs db.create_all() instead of Alembic migrations.
FLASK_APPmain.pyFlask CLI entrypoint for flask db migrate / flask db upgrade.
LOG_LEVEL20 (INFO)Numeric root logger level; also the OTEL LoggingHandler level. Prod uses 10.
SOCKET_IP(unset)Bind host for socketio.run() in __main__ dev mode. .env.example: 0.0.0.0.
IS_LOCAL_ENVIRONMENT is derived (DEPLOYMENT_ENVIRONMENT == "local"), not a raw env var. Note DEPLOYMENT_ENVIRONMENT (local/server) and APP_ENVIRONMENT (dev/production) are two different switches — see Startup & validation.
VariableDefaultPurpose
DB_USERNAME""PostgreSQL user (.strip()ed). .env.example: idrobots.
DB_PASSWORD""PostgreSQL password. .env.example: idrobots.
DB_IP""PostgreSQL host. Criticalvalidate_critical_config() raises RuntimeError if empty. .env.example: skyhub-postgres.
DB_NAME""Database name. .env.example: skyhub.
Assembled into postgresql://… in src/connector/db_connection.py. See Migrations & DB connection and the Database schema reference.
VariableDefaultPurpose
JWT_SECRET_KEYtest-secret-key-for-development-only (local only)HS256 signing key for access/refresh JWTs and the Socket.IO handshake. Required — raises RuntimeError at import on non-local deployments; the insecure fallback is used only when IS_LOCAL_ENVIRONMENT.
JWT_ACCESS_TOKEN_EXPIRES (10 min) and JWT_REFRESH_TOKEN_EXPIRES (12 h) are hardcoded timedeltas in main.py, not env-configurable — despite older docs listing them as env vars. The JWT blocklist is an in-memory set; revocation does not survive a restart or scale across workers without a shared store.
VariableDefaultPurpose
REGIONeu-central-1AWS region. Critical — validated at startup. Used for S3/asset ops.
RESOURCE_TAGskyhub-devResource-tag prefix; used in drone_routes (installer, SSM /<RESOURCE_TAG>/pull_role).
ACCOUNT_ID123AWS account id; used in drone installer/ECR references.
ASSET_BUCKETskyhub-prod-assetsS3 bucket for drone/user assets (video, image, logs).
VPN_BUCKET(unset)S3 bucket of per-user/per-drone WireGuard access.conf. Critical on non-local deployments. Prod: skyhub-prod-user-vpn.
INSTALLER_BUCKETskyhubcoreS3 bucket holding docker-compose(.prod).yml handed to drones via presigned URL.
ECR_REPODOCKER_REPO<aws-account-id>.dkr.ecr.eu-central-1.amazonaws.comPrivate ECR registry returned to drones for image pulls.
AWS_ACCESS_KEY_ID(unset)boto3 credential (implicit) for S3 presign / asset ops.
AWS_SECRET_ACCESS_KEY(unset)boto3 credential (implicit).
AWS_DEFAULT_REGIONeu-central-1boto3 region auto-detect for the S3 client.
VariableDefaultPurpose
VPN_SERVICE_IP(unset)Host of the external WireGuard status service, queried by VPN_Service.
VPN_SERVICE_PORT5050Port of the external VPN status service.
USER_NETWORK_CIDR10.70.0.0/16WireGuard user subnet; IPService allocates user IPs here.
DRONE_NETWORK_CIDR10.71.0.0/16WireGuard drone subnet; middleware trusts 10.71.* sources as authenticated drones.
SITL_HOST<office-docker-host> (from DOCKER_HOST_IP)IP used to order/represent SITL drones in IPService.
The 10.71.* trust and jumphost routing are covered in VPN middleware & jumphost and Network topology.
VariableDefaultPurpose
JUMPHOST_IP"" (empty)If set, rosbridge Connections route through ws://JUMPHOST_IP:JUMPHOST_PORT and add x-drone-ip / x-drone-port headers. Empty string = direct connection to the drone IP.
JUMPHOST_PORT9090Port on the jumphost fronting rosbridge.
MAX_RECONNECT_ATTEMPTS15SmartSocket max reconnect attempts before is_expired.
MAX_RECONNECT_TIME60Max seconds disconnected before giving up (s).
DRONE_REACHABILITY_TIMEOUT2Connect/read timeout (s) for the HTTP probe before opening a rosbridge socket.
ROSBRIDGE_SERVICE_TIMEOUT30.0Timeout (s) for rosbridge service calls (mission upload, geofence, param) — raised to 30 for WireGuard latency.
REQUEUEfalseIf true, failed outgoing rosbridge messages are re-queued instead of dropped.
SEND_QUEUE_SIZE30Max size of the per-connection outgoing rosbridge queue. Prod: 200.
TELEMETRY_THROTTLE_RATE200Min interval (ms) between telemetry messages via rosbridge throttle_rate; 0 disables throttling.
See Rosbridge connection.
VariableDefaultPurpose
SOCKETIO_PING_TIMEOUT60Flask-SocketIO ping timeout (s).
SOCKETIO_PING_INTERVAL25Flask-SocketIO ping interval (s).
SOCKETIO_MESSAGE_QUEUE(unset)Optional Redis URL for multi-worker fan-out. Required to scale beyond 1 gunicorn worker (rooms and the connection pool are per-process).
DO_UNSUBSCRIBEtrueIf true, unsubscribe_telemetry actually stops the underlying rosbridge subscription.
VariableDefaultPurpose
DOCKER_HOST(unset)Docker daemon socket/host for SITL & control services.
DOCKER_HOST_IP(unset)IP SITL containers connect back to; SITL drones use this instead of drone.ip. Also feeds SITL_HOST. .env.example: <office-docker-host>.
REMOTE_DOCKER_ENABLEDfalseProvision SITL containers on a remote Docker host and enable ECR-authenticated pulls.
REMOTE_DOCKER_HOSTssh://nexus0@<office-docker-host>Docker SDK base_url (ssh:// uses the system ssh client; tcp:// uses the entrypoint SSH tunnel).
SITL_IMAGE_NAMEardupilotArduPilot SITL image.
CORE_IMAGE_NAMEcore:latestCore (MAVROS + rosbridge) container image.
GAMEPAD_IMAGE_NAMEskyhub-gamepad:latestGamepad control container image.
USER_SITL_MAX_COUNT3Max SITL drones per user. .env.example / prod: 5.
SITL_CPU_LIMIT2.0CPUs per SITL container.
SITL_MEMORY_LIMIT4gMemory per SITL container.
CORE_CPU_LIMIT / CORE_MEMORY_LIMIT2.0 / 2gCore container cgroup limits.
GAMEPAD_CPU_LIMIT / GAMEPAD_MEMORY_LIMIT0.5 / 512mGamepad container cgroup limits.
GAMEPAD_API_URLhttp://localhost:5000Backend URL gamepad containers call for execution tracking (must be reachable from the remote Docker host).
SITL_REDIS_IMAGEredis:7-alpineRedis image for SITL container groups.
SITL_REDIS_HOSThost.docker.internalRedis host for SITL/gamepad containers.
REDIS_PASSWORDSITL_REDIS_PASSWORDskyhub_redis_secretRedis auth for SITL containers.
Full container lifecycle: SITL lifecycle and Gateway build & runtime.
Applied to every newly created SITL drone (fence + battery + logging defaults).
VariableDefaultPurpose
SITL_FENCE_ENABLE1FENCE_ENABLE.
SITL_FENCE_TYPE7FENCE_TYPE bitmask (alt + circle + polygon).
SITL_FENCE_ACTION1FENCE_ACTION (1 = RTL on breach).
SITL_FENCE_ALT_MAX100Max fence altitude (m).
SITL_FENCE_RADIUS300Circle fence radius (m).
SITL_BATT_VOLTAGE12.587Simulated 3S battery voltage.
SITL_BATT_CAPACITY3300Battery capacity (mAh).
SITL_BATT_LOW_VOLT10.5Low-voltage failsafe threshold.
SITL_BATT_CRT_VOLT9.6Critical-voltage failsafe threshold.
SITL_BATT_ARM_VOLT10.0Min voltage to allow arming.
SITL_BATT_FS_LOW_ACT2Low-battery failsafe action (2 = RTL).
LOG_ERASE_AFTER_DOWNLOADfalseErase FC logs after successful S3 upload.
VariableDefaultPurpose
JANUS_URL(unset)Janus Gateway REST base URL for video-room management. .env.example: http://janus-gateway:8088/janus.
WHIP_SERVER_URL(unset)WHIP server base URL for video ingest. .env.example: http://simple-whip-server:7080.
SITL_VIDEO_STREAM_DRONE_STATEVIDEO_STREAM_DRONE_STATEARMEDDrone state at which SITL video streaming starts. .env.example / prod: CONNECTED.
See Janus video rooms.
VariableDefaultPurpose
OTEL_EXPORTER_OTLP_ENDPOINT(unset)If unset, OTEL init is skipped entirely. Base OTLP-HTTP endpoint; exporters append /v1/traces and /v1/logs. Compose default: http://<office-docker-host>:4318.
OTEL_EXPORTER_OTLP_HEADERS(unset)Comma-separated key=value headers (e.g. signoz-access-token=<redacted>) attached to span + log exporters.
OTEL_SERVICE_NAMEskyhub_gateway_serviceservice.name resource attribute.
OTEL_RESOURCE_ATTRIBUTESdeployment.environment={DEPLOYMENT_ENVIRONMENT}Defined in settings.py but never appliedmain.py builds Resource() directly, so this var is silently dropped.
Path footgun: .env.example sets OTEL_EXPORTER_OTLP_ENDPOINT=http://<office-docker-host>:4318/v1/traces, but the exporters append /v1/traces and /v1/logs to the base — baking a signal-specific path into the base var makes the log exporter target /v1/traces/v1/logs. Use the bare base (http://host:4318). Details in OpenTelemetry & SigNoz.
VariableDefaultPurpose
STRIPE_SECRET_KEY""Server API key. Redact — use sk_test_* in dev, sk_live_xxx in prod.
STRIPE_PUBLISHABLE_KEY""Client key (pk_test_* / pk_live_xxx).
STRIPE_WEBHOOK_SECRET""Webhook signature secret (whsec_…).
STRIPE_PRICE_ID""Price id for the €120/vehicle/year subscription.
STRIPE_SUCCESS_URLhttps://skyhub.ai/billing/successCheckout success redirect.
STRIPE_CANCEL_URLhttps://skyhub.ai/billing/cancelCheckout cancel redirect.
See Stripe billing & vehicle limits.
VariableDefaultPurpose
TEST_EMAIL_SERVERtrueIf true, EmailService logs emails instead of sending via SMTP.
MAIL_SERVERsmtp.gmail.comSMTP host. Read directly in email_service.py, not in settings.py.
MAIL_PORT587SMTP port.
MAIL_USERNAME[email protected]SMTP sender.
MAIL_PASSWORD(unset / <redacted>)SMTP app password. Committed in .env.example for dev — redact in prod.
MAIL_* and APP_ENVIRONMENT bypass settings.py (read via os.getenv directly) — easy to miss when auditing configuration.
These are consumed by containers/docker-entrypoint.sh or docker-compose.ymlnot by settings.py. See Gateway build & runtime.
VariableDefaultPurpose
SSH_PRIVATE_KEY / SSH_PUBLIC_KEY / SSH_KNOWN_HOSTS(unset)Injected from SSM into /root/.ssh so the container can reach the remote Docker host.
REMOTE_DOCKER_SSH_TARGET(unset)Entrypoint-only. With REMOTE_DOCKER_ENABLED=true, opens an ssh -L 2375 tunnel to the remote daemon.
JUMPHOST_PUBLIC_IP<prod-ingress-ip>Entrypoint ssh ProxyCommand hop to reach the office Docker host (<office-docker-host>) from AWS.
PYTHONPATH/app/srcImport root so main:app / src.main:app resolve.
GATEWAY_PORT5000Host port mapping (compose).
GATEWAY_CLOUDFLARE_PORT2053Cloudflare-tunnel-friendly host port also mapped to 5000.

Dashboard (skyhub_dashboard)

The Angular SPA has no runtime env vars — configuration is a TypeScript object in src/environments/environment*.ts, and angular.json swaps one file in at build time via fileReplacements. Change a value → rebuild and redeploy. The five files and the build configuration that selects each:
Build configEnv fileproductionSelected by
production (default)environment.prod.tstruenpm run build (no args)
aws-devenvironment.aws-dev.tstrueng build --configuration aws-dev
developmentenvironment.tsfalseng serve (default)
localenvironment.local.tsfalsenpm run start:local / build:local
e2eenvironment.e2e.tstruePlaywright webServer

Keys and per-environment values

KeyPurposedev / localprod / e2eaws-dev
urlGateway REST base (/api/v1)http://localhost:5000/api/v1https://prod.skyhub.ai:5000/api/v1https://dev.skyhub.ai:5000/api/v1
janusGatewayUrlJanus WebSocket (WebRTC video)ws://localhost:8188 (local) / wss://prod.skyhub.ai:8188 (dev default)wss://prod.skyhub.ai:8188wss://dev.skyhub.ai:8188
ws_proxyGamepad WS proxy (redispad)ws://localhost:7070 (local) / wss://prod.skyhub.ai:7070 (dev default)wss://prod.skyhub.ai:7070wss://ws_proxy.skyhub-dev.internal:7070
janusIceServersSTUN/ICE servers['stun:stun.l.google.com:19302']samesame
assetsUrlS3 base for drone assetshttps://skyhub-prod-assets.s3.eu-central-1.amazonaws.com/drone (all envs)samesame
mapbox.accessTokenMapbox GL public tokenpk.eyJ1… (committed, all envs)samesame
stripePublishableKeyStripe publishable keypk_live_xxx (present only in environment.ts + environment.prod.ts)present (prod)absent
enableIsaacSimIsaac Sim feature flagfalsefalsefalse
sseDebounceTimeStream update debounce (ms)100010001000
httpSessionExpiryTimeClient session expiry (minutes)888
DEFAULT_LAT / DEFAULT_LNGInitial map center (Plovdiv, BG)42.1354 / 24.7453samesame
environment.ts is the dev default (production: false) yet it already carries the live Stripe key and the prod Janus / ws_proxy / assets URLs — only its url points at localhost. environment.e2e.ts deliberately targets the real production API (https://prod.skyhub.ai:5000), so Playwright smoke runs hit prod. Full build/deploy detail: Dashboard build & config · App state & video.

SkyCore Drone OS (skyhub_core)

The on-drone stack is a docker-compose microservice bundle configured through a single .env (template .env.example). Every value uses the ${VAR:-default} pattern, so unset vars fall back to the compose default. COMPOSE_PROFILES decides which containers even start. Below are the load-bearing groups; see Drone OS services for the exhaustive per-module treatment.
VariableDefaultPurpose
COMPOSE_PROFILESmavproxy,core,rtk,gamepadComma-separated container profiles to start. Options: mavproxy, core, camera, rtk, gamepad, slam.
SKYHUB_SERVER_URLhttp://whip.skyhub-prod.internal:7080WHIP ingest endpoint the on-drone video pipeline pushes H264 to.
API_URLhttps://prod.skyhub.ai:5000Gateway base for drone-originated callbacks (executions, assets).
ROS_DOMAIN_ID1ROS2 DDS domain id.
FCUURLudp://127.0.0.1:14550@MAVLink connection to the FCU (via mavp2p).
IP_OVERRIDEwg0 IP, else 10.223.x locallyOverrides the drone’s WireGuard IP used to namespace Redis channels ({ip}:gamepad_input, {ip}:aruco_tracking, …). Legacy typo IP_OVRIDE still honored.
IP_OVERRIDE is central to the Redis message bus — see Redis message bus and Redis channels & MAVLink port map.

AWS infrastructure (skyhub_terraform)

Terraform inputs live in environments/<env>/inputs.tf (declarations) and are set per environment in environments/prod/settings.tfvars (prod) — dev supplies values on the CLI. Applied with terraform apply -var-file=settings.tfvars.
VariableProd value / defaultPurpose
client_nameskyhubResource naming prefix.
client_descriptionSkyhubHuman label.
environmentprod (default dev)Environment discriminator (drives skyhub-prod-* naming).
environment_descriptionSkyhub Production CloudHuman label.
aws_regioneu-central-1Target AWS region.
github_token<redacted>PAT for CodeBuild source access. Committed in settings.tfvars — rotate/redact.
slack_webhook<redacted>Slack incoming webhook for build notifications. Committed — redact.
video_port_start / video_port_end10000 / 10099Janus primary RTP media port range.
video_port_start_2 / video_port_end_220000 / 20099Secondary media/relay port range.
custom_ssh_port3377Non-standard SSH port on the WireGuard/jumphost instance.
The provisioned topology (ECS Fargate on 172.31.0.0/16, the single t4g.nano WireGuard jumphost at public <prod-ingress-ip>, self-hosted Postgres/Redis) is described in AWS environments & state and Production config. Prod ingress is nginx running on that t4g.nano WireGuard EC2, acting as a TLS-terminating reverse proxy in front of the ECS services — there is no Cloudflare tunnel or ALB/ELB in prod. GATEWAY_CLOUDFLARE_PORT / 2053 is only a local/optional dev-compose artifact and is not used in production.

Secrets hygiene

Several real secrets are committed in these repos as dev conveniences and must be overridden in production (via SSM for the gateway, CI secrets for the frontend, and a non-committed tfvars for Terraform):
  • Gateway .env.example: a working JWT_SECRET_KEY, MAIL_PASSWORD, and an OTEL signoz-access-token.
  • Dashboard environment.ts / environment.prod.ts: a live Stripe publishable key and the Mapbox token (baked into the shipped JS bundle).
  • Terraform settings.tfvars: a GitHub PAT and a Slack webhook.
Production gateway secrets come from AWS SSM Parameter Store, injected into the ECS task — never from the committed .env.example. settings.py hard-fails on a missing JWT_SECRET_KEY whenever DEPLOYMENT_ENVIRONMENT != local, which is the primary guard against shipping the insecure default.

Gateway env vars (detailed)

Startup validation order, footguns, and the code path reading each gateway value.

Dashboard build & config

The five Angular environment files, build configurations, and CI/deploy flow.

Drone OS services

On-drone container profiles and how COMPOSE_PROFILES selects them.

Environments & Terraform state

tfvars, remote state, and the prod/dev environment split.

HTTP & Socket.IO API reference

Full endpoint and event reference for the gateway.

Redis channels & MAVLink ports

Where IP_OVERRIDE, ports, and channel naming come together.