Skip to main content

Configuration

The Airborne server is configured entirely through environment variables, parsed at boot by AppConfig::build (in airborne_server/src/config.rs). This page is the authoritative reference. The example values come from airborne_server/.env.example; the parsing rules, defaults, and "required" markings come from config.rs itself.

A few rules apply across every variable:

  • Required means the server panics on boot if the variable is missing (or, for secrets, fails to decrypt). "Required" below reflects what config.rs enforces; some variables are conditionally required — see the notes.
  • An empty value is treated as unset for most string variables (get_env/get_optional ignore empty strings).
  • Variables marked as secret are decrypted on boot when USE_ENCRYPTED_SECRETS=true; otherwise they are read as plaintext.
  • Numeric and boolean variables fall back to their default if the value fails to parse (they do not error).
tip

For local development you rarely set these by hand. make setup seeds airborne_server/.env from .env.example, and the init scripts fill in the generated values (Keycloak secrets, Superposition org id, KMS-encrypted master key). See Run locally.

Server

VariableRequiredDefault / ExamplePurpose
PORTNo8081TCP port the HTTP server binds on (0.0.0.0:PORT).
SERVER_PATH_PREFIXNoapiPath prefix for the management API and the health route. The health check is GET /{SERVER_PATH_PREFIX}/health.
KEEP_ALIVENo30Actix-web keep-alive timeout, in seconds.
BACKLOGNo1024Listen backlog (max pending connections).
ACTIX_WORKERSNo4Number of Actix worker threads.
PUBLIC_ENDPOINTYeshttp://localhost:3000Externally reachable base URL of the deployment. Used by the dashboard to reach the API and to build browser-facing URLs / redirects.
RUST_LOGNodebug,info,error,actix_web=info,errorStandard Rust log filter directive.
LOG_FORMATNo(empty)Tracing output format. Empty selects the default human-readable format; set to a JSON-style value for structured logs.

Database

The server connects to PostgreSQL for application data and Casbin policies. There are two credential sets: a runtime user and a migration user (the migration user runs schema changes and is granted broader privileges).

VariableRequiredDefault / ExamplePurpose
DB_USERYespostgresRuntime database user.
DB_PASSWORDYes (secret)postgresRuntime user password. Decrypted when USE_ENCRYPTED_SECRETS=true.
DB_MIGRATION_USERYespostgresUser used to run Diesel migrations.
DB_MIGRATION_PASSWORDYes (secret)postgresMigration user password. Decrypted when USE_ENCRYPTED_SECRETS=true.
DB_HOSTYeslocalhostDatabase host.
DB_PORTYes5433Database port (the local Postgres container maps 54335432).
DB_NAMEYeshyperotaserverDatabase name.
DB_URLNopostgres://postgres:postgres@localhost:5433/hyperotaserverOptional full connection URL. When set, used directly instead of assembling from the parts above.
DB_MIGRATION_URLNo(unset)Optional full connection URL for the migration connection.
DATABASE_POOL_SIZENo4 (.env.example sets 2)Connection-pool size. Note the code default is 4; the example file overrides it to 2.
note

DB_URL / DB_MIGRATION_URL are optional overrides. If you provide them you can omit the corresponding discrete parts, but DB_USER, DB_HOST, DB_PORT, and DB_NAME are still read as required by config.rs, so keep them set. The make db-migration target separately reads DATABASE_URL/DB_URL from .env for the Diesel CLI.

Redis

Redis is an optional but recommended cache. It backs two things: the OIDC login PKCE + nonce hardening, and read caches for file, package, and workspace lookups.

VariableRequiredDefault / ExamplePurpose
REDIS_URLNoredis://localhost:6379Redis connection string. When set, the server enables PKCE/nonce protection on the OIDC flow and caches hot reads. When unset, Redis is skipped entirely.

How it is used:

  • Auth hardening. When REDIS_URL is set, the login-initiation step generates a PKCE challenge and stores the code verifier and OIDC nonce in Redis (keyed by a hash of the OAuth state, 10-minute TTL, single-use). The callback consumes them to complete the PKCE exchange and verify the ID-token nonce.
  • Read caches. File-entry lookups (3-day TTL), package reads and workspace-name lookups (1-week TTL) are cached and invalidated on the corresponding writes.
What happens without Redis

If REDIS_URL is unset, OIDC login still works, but PKCE and nonce verification are silently skipped and no read caching happens. For production, deploy a Redis instance and set REDIS_URL to keep the auth flow hardened.

Malformed URL panics on boot

If REDIS_URL is set but the value is invalid or the instance is unreachable at startup, the server panics on boot. Leave it unset to disable Redis; only set it to a working endpoint.

Local development

make run starts a bundled Redis via docker-compose. REDIS_PORT (default 6379) is a docker-compose-only variable that maps the container port to the host; the server itself only reads REDIS_URL. make redis / make redis-insight start Redis and the RedisInsight UI on their own. See Run locally.

Authentication / OIDC

Authentication is OIDC-based. AUTHN_PROVIDER selects the flavour; the shared OIDC_* variables describe the issuer and client; the AUTH_ADMIN_* variables back provider-specific signup and user-management flows (mandatory for Keycloak).

VariableRequiredDefault / ExamplePurpose
AUTHN_PROVIDERNokeycloakAuthentication provider. One of keycloak, oidc, okta, auth0. Invalid values panic on boot.
OIDC_ENABLED_IDPSNogoogleComma-separated IdP hints shown on the sign-in screen and mapped to provider icons (for example google,github,microsoft). Deduplicated and lowercased.
OIDC_ISSUER_URLYeshttp://localhost:8180/realms/hyperOTAOIDC issuer URL used to validate tokens. Required at boot (the server panics if unset).
OIDC_EXTERNAL_ISSUER_URLNohttp://localhost:8180/realms/hyperOTAIssuer/base URL used for browser redirects. Defaults to OIDC_ISSUER_URL. A scheme is added if missing.
OIDC_CLIENT_IDYeshyperotaOIDC client id. Required at boot.
OIDC_CLIENT_SECRETYes (secret)get-secret-from-keycloakOIDC client secret. Required at boot; decrypted when secrets are encrypted.
OIDC_CLOCK_SKEW_SECSNo60Allowed JWT clock skew (leeway) in seconds for exp/nbf validation.
AUTH_ADMIN_CLIENT_IDConditionalhyperotaAdmin API client id. Required when AUTHN_PROVIDER=keycloak (otherwise optional).
AUTH_ADMIN_CLIENT_SECRETConditional (secret)get-admin-secret-from-keycloakAdmin API client secret. Required when AUTHN_PROVIDER=keycloak; decrypted when secrets are encrypted.
AUTH_ADMIN_TOKEN_URLConditionalhttp://localhost:8180/realms/hyperOTA/protocol/openid-connect/tokenToken endpoint for the admin client. Required when AUTHN_PROVIDER=keycloak.
AUTH_ADMIN_ISSUERConditionalhttp://localhost:8180/realms/hyperOTAKeycloak realm URL (https://<host>/[base-path]/realms/<realm>). Parsed into host + realm. Required when AUTHN_PROVIDER=keycloak; an invalid format panics.
AUTH_ADMIN_AUDIENCENo(empty)Optional audience for admin token requests.
AUTH_ADMIN_SCOPESNo(empty)Optional scopes for admin token requests.
caution

When AUTHN_PROVIDER=keycloak, the server panics on boot if any of AUTH_ADMIN_CLIENT_ID, AUTH_ADMIN_CLIENT_SECRET, AUTH_ADMIN_TOKEN_URL, or a parseable AUTH_ADMIN_ISSUER is missing. For non-Keycloak providers these are optional. AUTH_ADMIN_ISSUER must be a realm URL containing /realms/<realm>.

Authorization / Casbin

Authorization uses Casbin, with policies persisted in Postgres. These variables control the authorization provider, super-admin bootstrapping, and policy reload cadence.

VariableRequiredDefault / ExamplePurpose
AUTHZ_PROVIDERNocasbinAuthorization provider. Only casbin is supported; other values panic on boot.
AUTHZ_BOOTSTRAP_SUPER_ADMINSNo(empty)Comma-separated list of identities granted super-admin on boot. Trimmed, lowercased, empties dropped.
AUTHZ_CASBIN_AUTOLOAD_SECSNo60Interval (seconds) at which Casbin reloads policies from the database. Only applied if it parses as a positive integer.

Superposition

Airborne uses Superposition as its configuration/feature-flag engine for dimensions and release targeting. The base URL and org id are required; the token variables are only needed when Superposition is run in authenticated mode.

VariableRequiredDefault / ExamplePurpose
SUPERPOSITION_URLYeshttp://localhost:8080Base URL of the Superposition service.
SUPERPOSITION_ORG_IDYesget-org-id-from-superpositionSuperposition organisation id. Populated by init-superposition.sh in dev.
ENABLE_AUTHENTICATED_SUPERPOSITIONNofalseWhen true, the Superposition SDK client sends auth tokens/cookies; this makes the three token variables below mandatory.
SUPERPOSITION_TOKENConditional (secret)(unset)Bearer token for Superposition. Used as the SDK bearer token (empty string if unset).
SUPERPOSITION_USER_TOKENConditional (secret)(unset)User cookie token. Required when ENABLE_AUTHENTICATED_SUPERPOSITION=true (panics if missing in that mode).
SUPERPOSITION_ORG_TOKENConditional (secret)(unset)Org cookie token. Required when ENABLE_AUTHENTICATED_SUPERPOSITION=true (panics if missing in that mode).
SUPERPOSITION_CLEAR_UNUSED_PROVIDERSNofalseWhen true, a background task evicts idle per-workspace Superposition providers (see below). When false, providers live for the process lifetime.
SUPERPOSITION_UNUSED_PROVIDER_TTLNo43200Idle time, in seconds, after which an unused workspace provider is evicted. Only applied when eviction is enabled. Default is 12 hours.
SUPERPOSITION_UNUSED_PROVIDER_CHECK_INTERVALNo1500How often, in seconds, the eviction sweep runs. Only applied when eviction is enabled. Default is 25 minutes.
SUPERPOSITION_MIGRATION_STRATEGYNoPATCHStrategy used when reconciling Superposition default configs during the superposition boot migration.

Airborne serves release config by resolving each workspace's (organisation + application) configuration live against the Superposition API. To avoid re-creating the SDK client on every request, the server caches one provider per workspace in an in-memory registry. Enabling SUPERPOSITION_CLEAR_UNUSED_PROVIDERS starts a background sweep that drops providers that have not been accessed within SUPERPOSITION_UNUSED_PROVIDER_TTL, checked every SUPERPOSITION_UNUSED_PROVIDER_CHECK_INTERVAL. This bounds memory on servers hosting many rarely-served workspaces.

Use the plural env-var name

The server reads SUPERPOSITION_CLEAR_UNUSED_PROVIDERS (plural). The bundled .env.example currently writes it as SUPERPOSITION_CLEAR_UNUSED_PROVIDER (singular), which the server ignores — so a value set on the singular key has no effect. Always set the plural name.

AWS / S3 / KMS / LocalStack

The server talks to S3 (object storage) and KMS (secret decryption). AWS credentials and region are read by the AWS SDK from the standard environment variables. In local development LocalStack stands in for both S3 and KMS via AWS_ENDPOINT_URL.

VariableRequiredDefault / ExamplePurpose
AWS_BUCKETYeshyper-ota-bucketS3 bucket for files, packages, and resources.
AWS_REGIONYes (SDK)us-east-1AWS region. Read by the AWS SDK credential/region chain.
AWS_ENDPOINT_URLNohttp://localhost:4566Custom S3/KMS endpoint. When set, the S3 client switches to path-style addressing (needed for LocalStack). Leave unset in production to use real AWS.
AWS_ACCESS_KEY_IDConditional (SDK)testAWS access key. Read by the SDK. In production, prefer an IAM role over static keys.
AWS_SECRET_ACCESS_KEYConditional (SDK)testAWS secret key. Read by the SDK. Prefer IAM roles in production.
AWS_SESSION_TOKENNo (SDK)testOptional session token for temporary credentials.
CLOUDFRONT_DISTRIBUTION_IDNoYOUR_CLOUDFRONT_DISTRIBUTION_IDCloudFront distribution id used for cache invalidations. Defaults to empty string if unset.
note

AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN are not parsed by config.rs directly — they are consumed by the AWS SDK's default credential and region providers (aws_config::from_env()). On ECS/EKS you typically drop the static keys and grant the task/pod an IAM role instead; only AWS_REGION and AWS_BUCKET remain required, plus AWS_ENDPOINT_URL left unset.

Google Sheets (optional)

These are only used when ORGANISATION_CREATION_DISABLED=true, in which case the server reads an allow-list / org mapping from a Google Sheet and needs Google service-account credentials.

VariableRequiredDefault / ExamplePurpose
GOOGLE_SPREADSHEET_IDConditional1mFqLcqr1pErYe2jc_eLaXjOGlWIGwVgBjoAkVh_P5RcSpreadsheet id read when org creation is disabled. Required when ORGANISATION_CREATION_DISABLED=true (panics otherwise).
GCP_SERVICE_ACCOUNT_PATHConditional/app/airborne-gcp.jsonFilesystem path to a Google service-account JSON. Used when org creation is disabled.
GOOGLE_SERVICE_ACCOUNT_KEYConditional (secret)(unset)Service-account JSON provided inline as an (encrypted) env var, as an alternative to GCP_SERVICE_ACCOUNT_PATH. Decrypted when secrets are encrypted.
note

When ORGANISATION_CREATION_DISABLED=true, the server requires GOOGLE_SPREADSHEET_ID and a usable credential source — either GCP_SERVICE_ACCOUNT_PATH (a readable file) or GOOGLE_SERVICE_ACCOUNT_KEY (inline JSON). If neither yields valid credentials, boot fails.

Feature flags

VariableRequiredDefault / ExamplePurpose
ORGANISATION_CREATION_DISABLEDNofalseWhen true, disables self-service org creation and switches to the Google Sheets allow-list model (see above).
ENABLE_GOOGLE_SIGNINNo (legacy)falseLegacy flag kept for backward compatibility. If OIDC_ENABLED_IDPS is unset and this is true, google is added to the enabled IdPs. Prefer OIDC_ENABLED_IDPS.
USE_LEGACY_BUILD_PACKAGESNofalseWhen true, each Android build is published under both the new and the legacy Maven coordinates/paths, for backward compatibility with un-migrated Gradle clients. See the note below.
Legacy Android build coordinates

When USE_LEGACY_BUILD_PACKAGES=true, every Android build is uploaded to two coordinate sets (identical AAR bytes, with a layout-specific POM and maven-metadata.xml for each):

New (default)Legacy
groupId{org}{org}.{app}
artifactId{app}-airborne-assetsairborne-assets

Enable this only while you still have Android clients that resolve the old {org}.{app}:airborne-assets coordinates; new clients should declare {org}:{app}-airborne-assets. The AAR convenience endpoint (GET /build/{org}/{app}/aar) always serves the new-layout artifact regardless of this flag; the legacy coordinates are reachable only via the Maven/S3 repository paths.

Encryption

Airborne can store its secrets encrypted at rest in the environment using envelope encryption: a random Data Encryption Key (DEK, the "master key") encrypts each secret with AES-256-GCM, and the DEK itself is encrypted with KMS. At boot the server decrypts the master key via KMS, then uses it to decrypt each secret.

VariableRequiredDefault / ExamplePurpose
USE_ENCRYPTED_SECRETSNotrueMaster switch. When true, secret variables are treated as KMS-envelope-encrypted and decrypted on boot. When false, secrets are read as plaintext.
MASTER_KEYConditional (secret)(generated)The KMS-encrypted Data Encryption Key. Required when USE_ENCRYPTED_SECRETS=true — the server errors with MASTER_KEY must be set when USE_ENCRYPTED_SECRETS=true if absent. Ignored when encryption is off.

The secret variables that participate in encryption are:

  • DB_PASSWORD
  • DB_MIGRATION_PASSWORD
  • OIDC_CLIENT_SECRET
  • AUTH_ADMIN_CLIENT_SECRET
  • SUPERPOSITION_TOKEN
  • SUPERPOSITION_USER_TOKEN
  • SUPERPOSITION_ORG_TOKEN
  • GOOGLE_SERVICE_ACCOUNT_KEY

The encrypt-envs flow

airborne_server/scripts/encrypt-envs.sh produces an encrypted .env:

  1. Generates a random DEK and saves it locally to .masterkey.local (mode 600, never commit it).
  2. Encrypts the DEK with KMS (aws kms encrypt, key id defaults to alias/airborne-secrets for the standalone script; the LocalStack init script uses alias/my-local-key). The result is written as MASTER_KEY in .env.
  3. Encrypts each secret listed above with AES-256-GCM under the DEK, storing each as a { "nonce": ..., "ciphertext": ... } JSON blob.
  4. Sets USE_ENCRYPTED_SECRETS=true.

Run it directly, or use the Makefile targets:

# Encrypt (KMS + AES-GCM)
cd airborne_server && ./scripts/encrypt-envs.sh

# Or plaintext mode (no encryption)
cd airborne_server && ./scripts/encrypt-envs.sh --plaintext
caution

The DEK in .masterkey.local is the plaintext key that can decrypt every secret. Keep it out of git and out of images. In production, prefer sourcing each secret from a secrets manager (AWS Secrets Manager / SSM) injected at runtime, or keep USE_ENCRYPTED_SECRETS=true with MASTER_KEY supplied by your secret store and a real KMS key granted to the task/pod role.

To run without encryption locally, set USE_ENCRYPTED_SECRETS=false (secrets are then read verbatim):

make setup USE_ENCRYPTED_SECRETS=false
make run USE_ENCRYPTED_SECRETS=false

Boot-time migrations

MIGRATIONS_TO_RUN_ON_BOOT is a comma-separated list controlling which migrations the server runs at startup. It is parsed case-insensitively and trimmed.

VariableRequiredDefault / ExamplePurpose
MIGRATIONS_TO_RUN_ON_BOOTNodb,superposition (example) / "" (code default)Which boot migrations to run. Recognized tokens below. The compiled default is empty (no migrations); the example file sets db,superposition.
SUPERPOSITION_MIGRATION_STRATEGYNoPATCHStrategy applied by the superposition migration step (see Superposition).

Recognized tokens:

TokenEffect
dbRuns pending Diesel database migrations (embedded in the binary) before serving.
superpositionReconciles Superposition default configs using SUPERPOSITION_MIGRATION_STRATEGY. Boot panics if this migration fails.
keycloaktocasbinRuns a one-time Keycloak → Casbin authorization import on boot (applies changes). This also triggers DB migrations first.
note

The same Keycloak→Casbin import is also available as an explicit one-off command: run the server binary with authz-import-keycloak --dry-run (preview) or authz-import-keycloak --apply (apply). Use this instead of keycloaktocasbin in MIGRATIONS_TO_RUN_ON_BOOT when you want a controlled, out-of-band migration.

Metrics (optional)

The Airborne server can push its own Prometheus metrics to a Victoria Metrics instance. This is opt-in and independent of the analytics server below.

VariableRequiredDefault / ExamplePurpose
VICTORIA_METRICS_INSERT_URLNo(empty)Base URL of a Victoria Metrics instance. When non-empty, the server POSTs its metrics to {url}/api/v1/import/prometheus every 30 seconds. When empty, no metrics are pushed.

Today the exported metrics are the Redis cache counters — redis_cache_hits_total, redis_cache_misses_total, and redis_cache_fails_total — labelled by instance, key prefix, organisation, application, and key-hierarchy level (useful for gauging cache effectiveness). There is no scrape endpoint; delivery is push-only.

Analytics server

The analytics server (airborne_analytics_server) is an optional, separate service that ingests OTA events. It has its own configuration, parsed in airborne_analytics_server/src/common/config.rs. It supports two backends selected by LOGGING_INFRASTRUCTURE: Kafka + ClickHouse or Victoria Metrics + Grafana.

VariableRequiredDefault / ExamplePurpose
SERVER_PORTNo6400Port the analytics HTTP server binds on.
LOGGING_INFRASTRUCTURENovictoria-metricsBackend selector: kafka-clickhouse or victoria-metrics. Defaults to Victoria Metrics.
KAFKA_BROKERSNolocalhost:9092Kafka bootstrap brokers.
KAFKA_TOPICNoota-eventsKafka topic consumed for OTA events.
KAFKA_CONSUMER_GROUPNoota-analytics-consumerKafka consumer group id.
KAFKA_SECURITY_PROTOCOLNo(unset)Optional Kafka security protocol (for example SASL_SSL).
KAFKA_SASL_MECHANISMSNo(unset)Optional SASL mechanism (for example PLAIN).
KAFKA_SASL_USERNAMENo(unset)Optional SASL username.
KAFKA_SASL_PASSWORDNo(unset)Optional SASL password.
CLICKHOUSE_URLNohttp://localhost:8123ClickHouse HTTP endpoint.
CLICKHOUSE_DATABASENoanalyticsClickHouse database name.
CLICKHOUSE_USERNAMENo(unset)Optional ClickHouse username.
CLICKHOUSE_PASSWORDNo(unset)Optional ClickHouse password.
RUST_LOGNoinfo,analytics=debug,rdkafka=info,clickhouse=debugLog filter for the analytics service.
caution

The analytics .env.example also lists DEFAULT_TENANT_ID, and the stack-specific env files (.env.victoria-metrics, .env.kafka-clickhouse) use KAFKA_BROKER_URL and VICTORIA_METRICS_URL. These names are not read by config.rs in the current code — the parser reads KAFKA_BROKERS (not KAFKA_BROKER_URL) and has no Victoria-Metrics URL variable. Treat the table above (the variables config.rs actually parses) as authoritative, and the extra names as scaffolding for the Docker stacks.