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
| Variable | Type | Default |
|---|---|---|
AP_KEY | hex-encoded binary, ≥ 32 bytes decoded | unset (nil) |
AP_SALT | hex-encoded binary | unset (nil) |
AP_ALLOW_INSECURE | boolean | false |
AP_SOURCE_ALLOWLIST | comma-separated list | [] |
AP_LOCAL_ROOT | existing directory, not / | unset (nil) — local sources disabled |
AP_VARIANT_STORE | scheme-tagged URL (file:///path, s3://bucket) | unset (nil) — no variant cache |
AP_MAX_CONCURRENCY | positive integer | System.schedulers_online/0 |
AP_MAX_PROBE_CONCURRENCY | positive integer | 4 × AP_MAX_CONCURRENCY |
AP_QUEUE_SIZE | non-negative integer | 32 |
AP_READY_QUEUE_THRESHOLD | non-negative integer, ≤ AP_QUEUE_SIZE | half the queue, floored, min 1 (0 disables) |
AP_MAX_SRC_BYTES | positive integer | 2_000_000_000 |
AP_MAX_VARIANT_BYTES | positive integer | the effective AP_MAX_SRC_BYTES |
AP_RENDER_TIMEOUT | positive integer (seconds) | 300 |
AP_PROBE_TIMEOUT | positive integer (seconds) | 10 |
AP_SERVE_MODE | redirect | proxy | :redirect |
AP_PRESIGN_TTL | positive integer (seconds) | 300 |
AP_LOG_LEVEL | debug | info | warning | error | :info |
AP_METRICS_BIND | IP address literal | 127.0.0.1 |
AP_METRICS_PORT | positive integer | 9568 |
AP_S3_ENDPOINT | http(s)://host[:port] | unset (nil) — AWS proper |
AP_S3_ADDRESSING | virtual | path | :virtual, or :path with an endpoint |
AP_S3_CA_BUNDLE | readable PEM file | unset (nil) — system trust store |
AP_VARIANT_S3_ENDPOINT | http(s)://host[:port] | the source-side endpoint |
AP_VARIANT_S3_ADDRESSING | virtual | path | derived; see below |
AP_VARIANT_S3_CA_BUNDLE | readable PEM file | the source-side bundle |
AP_VARIANT_S3_ACCESS_KEY_ID | string | the source-side credential |
AP_VARIANT_S3_SECRET_ACCESS_KEY | string | the source-side credential |
AP_VARIANT_S3_REGION | string | the source-side credential |
AP_VARIANT_S3_SESSION_TOKEN | string | the 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_KEYandREGIONmust all be set, andSESSION_TOKENis then optional and read only fromAP_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_ENDPOINTset andAP_VARIANT_S3_ADDRESSINGunset, 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 explicitAP_S3_ADDRESSINGfrom 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
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
@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.
@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
@spec all() :: t()
Returns the whole stored config.
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.
Returns one stored config value.
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.
@spec log_levels() :: [atom()]
The levels AP_LOG_LEVEL accepts.
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.
@spec s3_addressing_styles() :: [atom()]
The addressing styles AP_S3_ADDRESSING accepts.
@spec serve_modes() :: [atom()]
The serve modes AP_SERVE_MODE accepts.
@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.