AudioProxy.Config (audio_proxy v0.7.0)

Copy Markdown View Source

Runtime configuration, read from AP_-prefixed environment variables.

The variable surface is defined by docs/audio-proxy-api-v1.md §6. Values are parsed and validated exactly once, during AudioProxy.Application.start/2, and stored in :persistent_term — reads on the request path are then a cheap global lookup with no GenServer in the way.

Malformed values raise AudioProxy.Config.Error at boot, so a misconfigured container fails immediately instead of serving traffic with surprising defaults. Every error message names the offending variable.

Variables

VariableTypeDefault
AP_KEYhex-encoded binary, ≥ 32 bytes decodedunset (nil)
AP_SALThex-encoded binaryunset (nil)
AP_ALLOW_INSECUREbooleanfalse
AP_SOURCE_ALLOWLISTcomma-separated list[]
AP_LOCAL_ROOTexisting directory, not /unset (nil) — local sources disabled
AP_VARIANT_STOREscheme-tagged URL (file:///path, s3://bucket)unset (nil) — no variant cache
AP_MAX_CONCURRENCYpositive integerSystem.schedulers_online/0
AP_MAX_PROBE_CONCURRENCYpositive integer4 × AP_MAX_CONCURRENCY
AP_QUEUE_SIZEnon-negative integer32
AP_READY_QUEUE_THRESHOLDnon-negative integer, ≤ AP_QUEUE_SIZEhalf the queue, floored, min 1 (0 disables)
AP_MAX_SRC_BYTESpositive integer2_000_000_000
AP_MAX_VARIANT_BYTESpositive integerthe effective AP_MAX_SRC_BYTES
AP_RENDER_TIMEOUTpositive integer (seconds)300
AP_PROBE_TIMEOUTpositive integer (seconds)10
AP_SERVE_MODEredirect | proxy:redirect
AP_PRESIGN_TTLpositive integer (seconds)300
AP_LOG_LEVELdebug | info | warning | error:info
AP_METRICS_BINDIP address literal127.0.0.1
AP_METRICS_PORTpositive integer9568
AP_S3_ENDPOINThttp(s)://host[:port]unset (nil) — AWS proper
AP_S3_ADDRESSINGvirtual | path:virtual, or :path with an endpoint
AP_S3_CA_BUNDLEreadable PEM fileunset (nil) — system trust store
AP_VARIANT_S3_ENDPOINThttp(s)://host[:port]the source-side endpoint
AP_VARIANT_S3_ADDRESSINGvirtual | pathderived; see below
AP_VARIANT_S3_CA_BUNDLEreadable PEM filethe source-side bundle
AP_VARIANT_S3_ACCESS_KEY_IDstringthe source-side credential
AP_VARIANT_S3_SECRET_ACCESS_KEYstringthe source-side credential
AP_VARIANT_S3_REGIONstringthe source-side credential
AP_VARIANT_S3_SESSION_TOKENstringthe source-side token with the rest of an inherited identity; unset once the store has its own
AP_ALLOW_ORIGIN* or scheme://host[:port]unset (nil) — no CORS headers

The listener port is read from AP_PORT, falling back to PORT (which the worktree workflow sets to the branch's hashed port), then to 4000.

The AWS variables

S3 credentials are the one thing here not AP_-prefixed: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN and AWS_REGION (or AWS_DEFAULT_REGION). Every tool that produces credentials already writes those names, and renaming them would mean an operator translating a working environment into this proxy's dialect for no gain.

They are read here rather than left to ex_aws's own resolution, which would otherwise reach into application env and, on EC2, into IMDS. Keeping them in this map means the whole configuration surface is still one thing, validated once, at boot — and that what the proxy will use is what the operator set, not whatever a metadata endpoint happened to answer. AudioProxy.S3 hands them to ex_aws per request.

Validated all-or-nothing: a half-configured client fails at its first request instead of at boot, which is the failure this module exists to prevent.

The two S3 profiles

Reading sources and running the variant store are two jobs, and a deployment may want to give them different providers (sources on R2, variants on AWS) or different principals (a read-only credential for the source buckets, a writing one for the store). So the config carries two profiles of the same shape: :s3, read by everything that fetches a source, and :variant_s3, read by every variant-store operation.

AP_VARIANT_S3_* is an override group. Each variable that is unset falls back to the source-side value, so a deployment that sets none of them has two identical profiles and behaves exactly as it did before the split.

Two rules make that fallback well-behaved:

  • Credentials are group-atomic. Set any of the variant credential variables and the identity is the variant's own: ACCESS_KEY_ID, SECRET_ACCESS_KEY and REGION must all be set, and SESSION_TOKEN is then optional and read only from AP_VARIANT_S3_SESSION_TOKEN — a token belongs to the principal that minted it, so the source's is not carried onto another identity. Set none of them and the whole source identity is inherited, token included, which is what makes an un-overridden deployment identical to the pre-split one. A store half-borrowing the source's identity is never a deployment intent, so the partial set aborts boot naming exactly what is missing.

  • Addressing derives per side. With AP_VARIANT_S3_ENDPOINT set and AP_VARIANT_S3_ADDRESSING unset, the style is derived from the variant endpoint by the same rule the shared pair uses — so an R2 store behind AWS sources gets path-style without being told. With no variant endpoint there is nothing to derive from that the source has not already derived from, so the source's effective style is inherited whole; that is what keeps an explicit AP_S3_ADDRESSING from being silently dropped on the store side.

There is no way to spell "the variant store is on AWS proper while the sources are not" by unsetting the endpoint — an unset override means inherit. Write it out instead: AP_VARIANT_S3_ENDPOINT=https://s3.<region>.amazonaws.com with AP_VARIANT_S3_ADDRESSING=virtual, which is the request AWS's own default produces. docs/s3-providers.md has the worked example.

Summary

Types

The S3 settings, as one group.

t()

Functions

Returns the whole stored config.

Builds a validated config map from an environment map, without storing it.

Returns one stored config value.

Parses and validates the environment, then stores the result for all/0.

The levels AP_LOG_LEVEL accepts.

Replaces the stored config wholesale.

The addressing styles AP_S3_ADDRESSING accepts.

The serve modes AP_SERVE_MODE accepts.

The AP_-prefixed environment variables this module reads.

Types

s3()

@type s3() :: %{
  region: String.t() | nil,
  access_key_id: String.t() | nil,
  secret_access_key: String.t() | nil,
  session_token: String.t() | nil,
  endpoint: URI.t() | nil,
  addressing: :virtual | :path,
  ca_bundle: String.t() | nil
}

The S3 settings, as one group.

All of region, access_key_id and secret_access_key are nil together when S3 is not configured — the partial set is refused at boot, so anything that finds one of them finds all three.

addressing and ca_bundle are independent of the credentials: both have a usable value whether or not S3 is configured at all.

Two of these are built, not one: see "The two S3 profiles" above.

t()

@type t() :: %{
  port: pos_integer(),
  key: binary() | nil,
  salt: binary() | nil,
  allow_insecure: boolean(),
  source_allowlist: [String.t()],
  local_root: String.t() | nil,
  variant_store: AudioProxy.VariantStore.config() | nil,
  max_concurrency: pos_integer(),
  max_probe_concurrency: pos_integer(),
  queue_size: non_neg_integer(),
  ready_queue_threshold: non_neg_integer(),
  max_src_bytes: pos_integer(),
  max_variant_bytes: pos_integer(),
  render_timeout: pos_integer(),
  probe_timeout: pos_integer(),
  serve_mode: :redirect | :proxy,
  presign_ttl: pos_integer(),
  log_level: :debug | :info | :warning | :error,
  metrics_bind: :inet.ip_address(),
  metrics_port: pos_integer(),
  allow_origin: String.t() | nil,
  s3: s3(),
  variant_s3: s3()
}

Functions

all()

@spec all() :: t()

Returns the whole stored config.

build!(env)

@spec build!(map()) :: t()

Builds a validated config map from an environment map, without storing it.

This is the function tests exercise for parsing and validation. It writes nothing to :persistent_term; it does touch the filesystem where a value is a filesystem claim — AP_LOCAL_ROOT must be an existing directory, and a file:// variant store is probed for writability.

It does not touch the network. An s3:// variant store is parsed and shape-checked here and probed by load!/1, after the config is stored — because the probe is an S3 request, and AudioProxy.S3 reads its credentials, endpoint and addressing from the very map being built. The asymmetry with the file:// probe is that one: a path is provable from itself, a bucket is not.

get(key)

@spec get(atom()) :: term()

Returns one stored config value.

load!(env \\ System.get_env())

@spec load!(map()) :: t()

Parses and validates the environment, then stores the result for all/0.

Called from AudioProxy.Application.start/2. Raises Error on the first unusable value.

log_levels()

@spec log_levels() :: [atom()]

The levels AP_LOG_LEVEL accepts.

put_all(config)

@spec put_all(t()) :: t()

Replaces the stored config wholesale.

Used by load!/1 at boot and by AudioProxy.ConfigHelper in tests. Nothing on the request path should call this.

s3_addressing_styles()

@spec s3_addressing_styles() :: [atom()]

The addressing styles AP_S3_ADDRESSING accepts.

serve_modes()

@spec serve_modes() :: [atom()]

The serve modes AP_SERVE_MODE accepts.

variables()

@spec variables() :: [String.t()]

The AP_-prefixed environment variables this module reads.

Published so the configuration tables in README.md and llms-full.txt can be checked against the implementation instead of against a reviewer's memory — a stale default in a file an agent reads instead of the code is a wrong answer delivered confidently.

Names only, not defaults: several defaults are derived rather than literal (AP_MAX_PROBE_CONCURRENCY is a multiple of AP_MAX_CONCURRENCY, AP_MAX_VARIANT_BYTES inherits the effective AP_MAX_SRC_BYTES), and the two documents render those differently on purpose, for different readers.

Excludes the AWS_* credentials variables, which are read here too but are not this proxy's to define; see the moduledoc.