apistockdocs
v0.4 GitHub apistock.dev
Technical/Configuration and security

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/app reads the environment (LoadConfig in config.go). Libraries take values as constructor arguments; architecture_test.go enforces it.
  • The app reads the process environment, not .env. aps dev parses .env and passes it to the processes it starts, with variables already set in your shell taking precedence. With plain go run, run set -a; . ./.env; set +a first. Docker Compose reads .env on its own, for the port variables.
  • Every error is reported at once. LoadConfig collects all problems and fails with invalid 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=/path reads the value from the file (config.Source.Secret, trailing newline trimmed). Setting both NAME and NAME_FILE fails with config: both variable and _FILE variant are set: NAME. Secrets are held as config.Secret, whose String and LogValue print [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=production tightens 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
esc
↑↓ move↵ openesc close