Environment variables
Every environment variable apistock reads, verified against the code: what reads it, its default, how it's validated, whether it's a secret, and where the value comes from. For a beginner's walkthrough of the credentials, see Every key and credential.
Three groups of programs read the environment, and each has its own variables:
| Reader | Where | Variables |
|---|---|---|
A generated app (cmd/api, cmd/migrate, cmd/seed) |
internal/app/config.go, social.go, passkeys.go, infra_mail.go |
App, database, authentication, email, telemetry |
Docker Compose, from the app's compose.yaml |
Interpolated by docker compose, which reads .env itself |
Compose ports |
aps, and the apistock repository's tests |
cli/, modules/postgres/pgtest, root compose.yaml |
CLI, tests |
How the app reads configuration#
- Only
internal/appreads the environment (LoadConfiginconfig.go). Libraries take values as constructor arguments;architecture_test.goenforces it. - The app reads the process environment, not
.env.aps devparses.envand passes it to the processes it starts, with variables already set in your shell taking precedence. With plaingo run, runset -a; . ./.env; set +afirst. Docker Compose reads.envon its own, for the port variables. - Every error is reported at once.
LoadConfigcollects all problems and fails withinvalid configuration:followed by one line per variable, so one start shows everything to fix. - Secrets can come from files. For variables marked Secret below,
NAME_FILE=/pathreads the value from the file (config.Source.Secret, trailing newline trimmed). Setting bothNAMEandNAME_FILEfails withconfig: both variable and _FILE variant are set: NAME. Secrets are held asconfig.Secret, whoseStringandLogValueprint[redacted]. - Empty means off; half-filled means stop. An optional feature with all its variables empty is off. Setting some of a feature's variables but not the rest fails at start with the missing names (ADR-0045).
APP_ENV=productiontightens rules. Marked Prod below: required values, https-only URLs, Mailpit refused.- Environment holds secrets and infrastructure only. Tunables such as code lifetimes and the email sender are runtime settings in PostgreSQL, changed through
/ops/settings(runtime settings). A value is never in both.
App server#
Read in config.go by both presets.
| Variable | Required | Default | Example | Validation | Description |
|---|---|---|---|---|---|
APP_ENV |
No | development |
production |
development or production |
Production switches logs to JSON, sends HSTS (365 days), enables a 5 s drain delay on shutdown, and applies every Prod rule on this page. The Dockerfile sets production |
APP_ADDR |
No | 127.0.0.1:8080 |
0.0.0.0:8080 |
host:port |
Listen address. Loopback by default so a development API isn't exposed on the network; containers need 0.0.0.0 (the Dockerfile sets it) |
APP_LOG_LEVEL |
No | info |
debug |
debug, info, warn, error |
Minimum slog level |
APP_DOCS_ENABLED |
No | true |
false |
true or false |
Serves /docs and /openapi.json |
APP_CORS_ORIGINS |
No | empty | https://app.example.com,http://localhost:3000 |
Comma-separated origins (scheme, host, optional port) | Browser origins allowed by CORS, trusted by the cross-origin protection, and accepted as return_to for Google and Apple web sign-in. Empty disables CORS |
APP_MAX_BODY_BYTES |
No | 1048576 |
5242880 |
Positive integer | Request body limit; larger bodies get 413 request_too_large |
Database#
Full preset only.
| Variable | Required | Default | Example | Secret | Validation | Description |
|---|---|---|---|---|---|---|
DATABASE_URL |
Yes | none | postgres://acme-api:acme-api@127.0.0.1:5432/acme-api?sslmode=disable |
Secret | Required by app.New, cmd/migrate and cmd/seed (DATABASE_URL is required); parsed and pinged by postgres.Open |
PostgreSQL connection URL (pgx format). Development value from compose.yaml; production from your database provider, with sslmode=require or verify-full. Errors never include the URL |
APP_DB_MAX_CONNS |
No | 10 |
20 |
1 to 1000 | Pool size per instance. Keep instances × this below the server's max_connections |
|
APP_JOB_WORKERS |
No | 10 |
25 |
1 to 10000 | Concurrent jobs per instance on the default queue |
Authentication#
Two-factor authentication#
| Variable | Required | Default | Example | Secret | Description |
|---|---|---|---|---|---|
AUTH_ENCRYPTION_KEYS |
Prod; also cmd/seed |
empty | k2:…,k1:… |
Secret | AES-256-GCM keys that encrypt TOTP secrets. Format: comma-separated id:base64, each key exactly 32 bytes after base64 decoding; ids unique. The first key encrypts, all decrypt. Empty in development turns authenticator apps off (503 mfa_unavailable) and aps dev fills it. Generate: echo "k1:$(openssl rand -base64 32)". Rotate with cmd/api rotate-auth-keys (secrets and keys) |
Passkeys#
Read in passkeys.go (ADR-0044). No secrets.
| Variable | Required | Default (development) | Example | Validation | Description |
|---|---|---|---|---|---|
WEBAUTHN_RP_ID |
No (empty in production turns passkeys off) | localhost |
example.com |
Required when any other WEBAUTHN_ variable is set |
Relying party ID: the registrable domain every passkey is bound to. Changing it invalidates existing passkeys |
WEBAUTHN_ORIGINS |
With WEBAUTHN_RP_ID |
http://localhost:8080,http://localhost:3000 |
https://example.com,https://app.example.com |
Each on the RP ID or a subdomain; Prod: https | Origins allowed in WebAuthn client data |
WEBAUTHN_APPLE_APP_IDS |
No | empty | ABCDE12345.com.example.app |
TEAMID.bundle.id, comma-separated |
Served in /.well-known/apple-app-site-association under webcredentials |
WEBAUTHN_ANDROID_APPS |
No | empty | com.example.app=SHA256:AB:…:EF+SHA256:12:…:56 |
package=SHA256:FP[+SHA256:FP…], comma-separated |
Served in /.well-known/assetlinks.json; also allows the apps' android:apk-key-hash: origins |
In development, when both WEBAUTHN_RP_ID and WEBAUTHN_ORIGINS are empty, the defaults above apply. In production, both empty means passkey endpoints answer 503 passkeys_unavailable.
Google and Apple#
Read in social.go (ADR-0046).
| Variable | Required | Default | Example | Secret | Description |
|---|---|---|---|---|---|
APP_PUBLIC_URL |
With Google, or Apple web sign-in, in production | http://localhost:8080 in development |
https://api.example.com |
No | The API's public origin: scheme and host, no path. Callback URLs are <APP_PUBLIC_URL>/v1/auth/{google,apple}/callback; the default return_to is <APP_PUBLIC_URL>/docs. Prod: https |
GOOGLE_CLIENT_ID |
Enables Google | empty | 123-abc.apps.googleusercontent.com |
No | Web application client ID. Also an accepted audience for native ID tokens (Android's serverClientId) |
GOOGLE_CLIENT_SECRET |
With GOOGLE_CLIENT_ID |
empty | GOCSPX-… |
Secret | Web client secret, for the authorization code exchange |
GOOGLE_IOS_CLIENT_ID |
No; needs GOOGLE_CLIENT_ID |
empty | 123-ios.apps.googleusercontent.com |
No | Accepted audience for ID tokens from the iOS app |
GOOGLE_ANDROID_CLIENT_ID |
No; needs GOOGLE_CLIENT_ID |
empty | 123-and.apps.googleusercontent.com |
No | Accepted audience for ID tokens from the Android app |
APPLE_TEAM_ID |
With any Apple variable | empty | ABCDE12345 |
No | iss of the generated client secret |
APPLE_KEY_ID |
With any Apple variable | empty | XYZ987WVU6 |
No | kid of the generated client secret |
APPLE_PRIVATE_KEY (or APPLE_PRIVATE_KEY_FILE) |
With any Apple variable | empty | -----BEGIN PRIVATE KEY-----… |
Secret | The .p8 Sign in with Apple key: PKCS #8 PEM, ECDSA P-256. Parsed at start |
APPLE_SERVICES_ID |
This or APPLE_BUNDLE_IDS |
empty | com.example.web |
No | Client ID for web sign-in |
APPLE_BUNDLE_IDS |
This or APPLE_SERVICES_ID |
empty | com.example.app,com.example.app.dev |
No | Client IDs for native sign-in |
.env.example lists APPLE_PRIVATE_KEY_FILE and mentions APPLE_PRIVATE_KEY in its comment: a file is the recommended form, because multi-line values don't survive most .env parsers and environment dashboards.
Email#
Read in config.go and infra_mail.go. infra_mail.go is replaced by aps add mail, so exactly one provider's variables apply.
| Variable | Required | Default | Example | Secret | Description |
|---|---|---|---|---|---|
MAIL_DELIVERY |
No | mailpit in development, provider in production |
provider |
No | mailpit or provider. Prod: mailpit is refused. Provider credentials are only required when delivery is provider |
MAILPIT_SMTP_ADDR |
No | 127.0.0.1:1025 |
127.0.0.1:1035 |
No | host:port of Mailpit's SMTP server, used when delivery is mailpit |
RESEND_API_KEY |
Resend, with delivery provider |
empty | re_… |
Secret | Resend API key, "Sending access" is enough |
SMTP_HOST |
SMTP, with delivery provider |
empty | smtp.postmarkapp.com |
No | SMTP server |
SMTP_PORT |
No | 587 |
465 |
No | A port number (SMTP_PORT must be a port number such as 587) |
SMTP_TLS |
No | starttls |
tls |
No | starttls, tls (implicit TLS, usually 465) or none (local servers only; SMTP_TLS must be starttls, tls or none) |
SMTP_USERNAME |
No | empty | apikey |
No | Enables SMTP AUTH |
SMTP_PASSWORD |
With SMTP_USERNAME |
empty | Secret | SMTP password or token |
The sender (mail.from_name, mail.from_email, mail.reply_to) is a runtime setting, not an environment variable (email).
Telemetry#
| Variable | Required | Default | Example | Description |
|---|---|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
No | empty | http://127.0.0.1:4318 |
Turns on OTLP/HTTP export of traces and metrics. Empty keeps telemetry in-process: logs still carry trace_id and span_id. aps dev --observability sets it to the local Grafana. The OpenTelemetry exporters read it, and the other standard OTEL_EXPORTER_OTLP_* variables (such as OTEL_EXPORTER_OTLP_HEADERS for an API key), directly |
Compose ports#
Read by docker compose from .env (and by aps dev's port check), never by the app. All bind to 127.0.0.1.
| Variable | Default | Service | Also change |
|---|---|---|---|
POSTGRES_PORT |
5432 |
postgres |
The port in DATABASE_URL |
MAILPIT_SMTP_PORT |
1025 |
mailpit SMTP |
The port in MAILPIT_SMTP_ADDR |
MAILPIT_WEB_PORT |
8025 |
mailpit web inbox |
Nothing |
GRAFANA_PORT |
3000 |
grafana (profile observability) |
Nothing |
OTLP_HTTP_PORT |
4318 |
grafana OTLP receiver |
Nothing; aps dev --observability points the app at it |
CLI#
Read by aps.
| Variable | Effect |
|---|---|
CI |
Any value: never prompt, as with --no-input |
ACCESSIBLE |
Any value: plain one-line prompts for screen readers, as with --plain |
NO_COLOR |
Any value: no colour in output |
GOSUMDB, GONOSUMDB, GOPRIVATE, GOINSECURE |
Read by aps upgrade through go: it refuses to fetch an earlier release from the module proxy when checksum verification is off for apistock.dev/cli |
Tests#
Read by tests in the apistock repository and in generated apps.
| Variable | Used by | Effect |
|---|---|---|
APISTOCK_TEST_DATABASE_URL |
pgtest (every database test) |
PostgreSQL server URL. Tests create a database per test from a migrated template and drop it afterwards. Unset: database tests skip |
APISTOCK_REQUIRE_DB |
pgtest |
1: a missing APISTOCK_TEST_DATABASE_URL fails instead of skipping. Set it whenever you claim tests pass |
APISTOCK_TEST_MAILPIT_SMTP |
modules/mail/smtp, example apps |
Mailpit SMTP address, such as 127.0.0.1:51025 in the repository |
APISTOCK_TEST_MAILPIT_URL |
Same | Mailpit web URL, such as http://127.0.0.1:58025, to read delivered messages |
APISTOCK_REQUIRE_MAILPIT |
Same | 1: missing Mailpit variables fail instead of skipping |
APS_E2E |
cli tests |
1: generate apps and run their test suites |
APS_E2E_DOCKER |
cli tests |
1: run aps dev in a new Full app against real Docker on free ports |
APISTOCK_POSTGRES_PORT, APISTOCK_MAILPIT_SMTP_PORT, APISTOCK_MAILPIT_WEB_PORT |
Root compose.yaml |
Host ports of the repository's test services: 55432, 51025, 58025 |
Minimal preset#
A Minimal app reads only the app server variables and OTEL_EXPORTER_OTLP_ENDPOINT; its .env.example also has GRAFANA_PORT and OTLP_HTTP_PORT for aps dev --observability.
Checked against .env.example#
Both Full golden apps' .env.example files (examples/full-single, examples/full-multi, identical) list every variable the app reads, with these notes:
| Variable | Note |
|---|---|
APPLE_PRIVATE_KEY |
Read by the code; mentioned in the comment above APPLE_PRIVATE_KEY_FILE rather than as its own line, by design |
*_FILE variants |
Supported for every secret; listed only for DATABASE_URL and the Apple key |
SMTP_* |
Appear in the aps:begin mail block only after aps add mail --provider smtp; a new app lists RESEND_API_KEY there |