Application configuration reference
The tables list the environment settings decoded by internal/app/config.
Defaults come from config.Defaults() without reading files, opening connections
or starting services. Tests check this reference against those defaults. The
configuration guide explains loading and overrides.
Unset APP_ENV_FILE loads an optional .env; a nonempty process value selects a
required file; an explicitly empty value disables dotenv loading. Set this selector
outside the file being selected. Existing process variables win, even when empty;
empty decoded fields with an envDefault still use that default. unset below
means an empty value, not a generated credential.
Lists use commas. Durations use Go units such as 250ms, 15m or 168h, not 7d.
Callbacks, native clients and definitions of named queues, disks and logging
channels remain Go values. Unknown environment keys are ignored: obsolete names
are not aliases. Update settings and restart processes; there is no live reload.
The quickstart overrides several defaults, including disabling storage. Full
.env.example is a reference, not a ready production profile. Container addresses,
secrets and enabled features must match the deployment. These tables describe the
application adapter; independent pkg clients may expose additional native options.
Find the setting you need#
| Task | Reference | Usage guide |
|---|---|---|
| Tune process budgets | Application and logging below | Lifecycle |
| Connect SQL or tune its pool | Database | Connections |
| Configure listener, CORS, or docs | HTTP and CORS | Routing |
| Enable cache | Optional cache | Cache |
| Run workers and recurring work | Queue and event settings | Queues, scheduling |
| Send mail or push | Mail and push settings | Mail, push |
| Choose asset visibility and URLs | Filesystems and media settings | Media |
| Investigate a request | Tracing/metrics plus log channels | Observability |
The tables below are the complete environment reference. Usage guides explain when
to change a value; keeping the Go default is usually enough. Avoid copying every
row into .env: publish connection information, credentials, intentional feature
choices, and the overrides your deployment actually needs.
Application and logging#
| Key | Default |
|---|---|
APP_NAME |
labena |
APP_ENV |
development |
APP_STARTUP_TIMEOUT |
30s |
APP_SHUTDOWN_TIMEOUT |
45s |
APP_CLEANUP_TIMEOUT |
10s |
MIGRATION_TIMEOUT |
5m |
LOG_CHANNELS |
stderr |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT |
http://localhost:4318/v1/logs |
LOG_VIEWER_ENABLED |
false |
LOG_VIEWER_USERNAME |
unset |
LOG_VIEWER_PASSWORD |
unset |
LOG_VIEWER_BUFFER_ENTRIES |
2000 |
LOG_VIEWER_BUFFER_BYTES |
8388608 |
Process selection lives in each main.go as explicit app.Processes flags. HTTP
and workers automatically include their local event consumers; a separate
cmd/scheduler runs recurring schedules. See entry points.
Startup and cleanup have separate budgets; the shutdown groups share the drain
budget. Queue shutdown must be shorter than app shutdown. Migration timeout includes
lock acquisition; migrate --timeout overrides it. Set APP_ENV=production in
production; migrate refresh is then rejected, including with --force.
LOG_CHANNELS selects named definitions in config/observability.go: stderr, stdout,
file and observability (OTLP/HTTP). Formats, paths and levels belong to those definitions.
The observability channel exports directly to the logs endpoint; it does not write
a file. Its service identity comes from APP_NAME and APP_ENV.
The optional /admin/logs/ viewer needs both credentials and the HTTP process.
It works without OTLP, tracing or metrics. Buffer entries must be 1–100000; the
record-byte budget must be 65536–67108864. Keep it behind HTTPS and restrict ingress
to operators. See the local viewer for scope
and retention details.
Relative paths resolve against the process working directory. See
lifecycle and logging.
Database#
| Key | Default |
|---|---|
DATABASE_NAME |
unset |
DATABASE_DRIVER |
postgres |
DATABASE_URL |
unset |
DATABASE_MAX_OPEN_CONNS |
20 |
DATABASE_MAX_IDLE_CONNS |
5 |
DATABASE_CONN_MAX_LIFETIME |
30m |
DATABASE_CONN_MAX_IDLE_TIME |
5m |
DATABASE_CONNECT_TIMEOUT |
5s |
DATABASE_PING_TIMEOUT |
5s |
DATABASE_SLOW_QUERY_THRESHOLD |
500ms |
A nonempty DATABASE_URL opens a connection; serving the installed modules requires
PostgreSQL and their applied migrations. DATABASE_NAME is an optional diagnostic
name, not a replacement for the database selected by the URL. The reusable database
package supports MySQL, but the installed Identity, Media and Notification schemas
are PostgreSQL. Switching the driver does not translate their SQL.
Budget pool limits across every API, worker and operator process. Startup validates and pings configured connections; migrations only run on explicit command. See database and migrations.
HTTP and CORS#
| Key | Default |
|---|---|
HTTP_READINESS_DEPENDENCIES |
false |
HTTP_READINESS_TIMEOUT |
1s |
HTTP_TRUSTED_PROXIES |
unset |
HTTP_DOCS_ENABLED |
true |
HTTP_ADDRESS |
:8080 |
HTTP_READ_HEADER_TIMEOUT |
5s |
HTTP_READ_TIMEOUT |
15s |
HTTP_WRITE_TIMEOUT |
15s |
HTTP_IDLE_TIMEOUT |
60s |
HTTP_SHUTDOWN_TIMEOUT |
10s |
HTTP_MAX_HEADER_BYTES |
1048576 |
HTTP_CORS_ENABLED |
true |
HTTP_CORS_ALLOW_ORIGINS |
* |
HTTP_CORS_ALLOW_METHODS |
GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS |
HTTP_CORS_ALLOW_HEADERS |
Origin,Content-Type,Authorization |
HTTP_CORS_EXPOSE_HEADERS |
X-Request-ID,X-API-Version |
HTTP_CORS_ALLOW_CREDENTIALS |
false |
HTTP_CORS_MAX_AGE |
12h |
HTTP_DOCS_ENABLED=true serves /docs and /openapi.json from the registered
operations. Disable it to hide both endpoints. No file generation is required.
There is one application listener. HTTP_READINESS_DEPENDENCIES=true probes the
configured database, queue Redis and limiter Redis under one shared deadline.
Cache, SMTP and telemetry are excluded; /healthz never probes dependencies.
A worker-only process has no HTTP health listener. See
readiness.
Origins, methods and header lists are comma-separated. Wildcard origins cannot
be combined with credentialed CORS. For a browser that needs to read request IDs,
X-Request-ID and X-API-Version are exposed by default through HTTP_CORS_EXPOSE_HEADERS. CORS is browser policy, not access
control; bearer authorization still applies. See middleware.
Administrative paths under /admin are excluded from business CORS. Both built-in
dashboards require their separate Basic Auth credentials, reject cross-origin
reads and mutations, disable caching and framing, and must be served over HTTPS
outside local development. Preserve the external Host through your ingress.
HTTP_TRUSTED_PROXIES is a comma-separated list of explicit IPv4/IPv6 CIDRs for
controlled ingress proxies. It is empty by default; /0, hostnames and wildcard
trust are rejected. Only a trusted immediate peer can supply X-Forwarded-For.
The server walks the chain from right to left through trusted hops and selects
the first untrusted address. It ignores Forwarded and X-Real-IP, never changes
Host or scheme from forwarding headers, and rejects malformed or duplicate
X-Forwarded-For fields from a trusted peer (maximum 32 addresses / 4096 bytes).
A missing header falls back to the immediate peer.
Secure the proxy-to-backend network and prevent untrusted workloads from using trusted source addresses. The public ingress must discard incoming forwarding claims and rebuild the chain; later trusted proxies append their observed peer. Use exact proxy ranges, not an entire shared cluster network. Client IP affects HTTP/OTP rate limits and session client metadata, never user authorization. See ingress and readiness.
Queues and local events#
| Key | Default |
|---|---|
QUEUE_DASHBOARD_ENABLED |
false |
QUEUE_DASHBOARD_USERNAME |
unset |
QUEUE_DASHBOARD_PASSWORD |
unset |
QUEUE_ROUTES |
unset |
QUEUE_REDIS_URL |
redis://127.0.0.1:6379/1 |
QUEUE_DEFAULT |
default |
QUEUE_CONCURRENCY |
4 |
QUEUE_STRICT_PRIORITY |
false |
QUEUE_SHUTDOWN_TIMEOUT |
15s |
QUEUE_DEFAULT_PRIORITY |
1 |
QUEUE_HIGH_PRIORITY |
3 |
EVENTS_DRIVER |
gochannel |
EVENTS_SHUTDOWN_TIMEOUT |
10s |
QUEUE_ROUTES maps exact task names to configured queues, for example
identity.welcome.v1=high,mail.send.v1=default. Unlisted tasks use QUEUE_DEFAULT.
The queue names are defined in config/queues.go; QUEUE_<NAME>_PRIORITY overrides
each weight. Priorities must be positive. Concurrency is total active tasks per
process, not per queue. Strict priority can starve lower-priority queues.
Retries and per-attempt timeouts belong to task options, not global environment
settings. Native retry/classification callbacks can be supplied in Go.
QUEUE_WEIGHTS, QUEUE_NAME and database-queue settings are not supported.
Command scheduling enables the app.command handler and allowlisted factories;
the scheduler switch separately starts recurring enqueue. Enable command handling
on every consuming worker. The notification dispatcher runs with workers without
the cron scheduler. Administrative CLI commands and the authenticated Asynq
HTTP dashboard are separately opt-in. Dashboard credentials are required when enabled.
EVENTS_DRIVER accepts only gochannel. Events and poison messages are process-local
and ephemeral. NATS/RabbitMQ development containers do not create application drivers.
See tasks, scheduling and
events.
Optional cache#
| Key | Default |
|---|---|
CACHE_DRIVER |
disabled |
CACHE_NAMESPACE |
default |
CACHE_MAX_ENTRY_BYTES |
1048576 |
CACHE_MEMORY_MAX_BYTES |
67108864 |
CACHE_MEMORY_CAPACITY |
10000 |
CACHE_REDIS_URL |
redis://127.0.0.1:6379/0 |
CACHE_DATABASE_TABLE |
cache_entries |
CACHE_OPERATION_TIMEOUT |
5s |
Only the selected cache driver's settings are validated. Redis/database zero limits and timeouts resolve to their owner defaults; memory requires positive capacity and entry size, while zero memory byte budget selects 64 MiB. Disabled cache and unselected driver settings do not require operational configuration. Selected namespace/table mistakes fail before connections open.
Drivers are disabled, memory, database and redis. Memory budgets apply per
process. Database cache uses the configured Bun pool; publish its table with
go run ./cmd/cli cache tables, then apply it with migrate up. Changing
CACHE_DATABASE_TABLE does not rename an existing table. Expired SQL rows need
explicit PruneExpired work; no cleaner starts automatically.
Use an explicit namespace per app/environment. Queue, cache and authentication limits must not share a Redis database: configuration rejects matching configured address/database pairs. Different DB numbers do not isolate resource pressure or outages. Do not flush Redis to resolve task or authentication failures. See cache.
Mail and mail views#
| Key | Default |
|---|---|
MAIL_HOST |
sandbox.smtp.mailtrap.io |
MAIL_PORT |
2525 |
MAIL_USERNAME |
unset |
MAIL_PASSWORD |
unset |
MAIL_FROM_ADDRESS |
no-reply@example.com |
MAIL_FROM_NAME |
unset |
MAIL_BRAND_NAME |
unset |
MAIL_ACCENT_COLOR |
#1D4ED8 |
MAIL_LOGO_URL |
unset |
MAIL_WEBSITE_URL |
https://example.com |
MAIL_SUPPORT_EMAIL |
support@example.com |
MAIL_FOOTER |
Account updates, thoughtfully delivered. |
Unset or blank MAIL_FROM_NAME and MAIL_BRAND_NAME inherit APP_NAME.
MAIL_LOGO_URL is optional; without it, mail uses text branding.
Immediate SMTP attachments default to 20 MiB total raw bytes. Override
config.MailConfig.MaxAttachmentBytes in Go if needed; zero uses the default.
Queued mail still rejects attachment readers and limits its encoded message to
64 KiB. SMTP options use the same pure validator as standalone mail.
The sender address is required by app; standalone SMTP may receive From per message.
Mail is always wired into the app. Startup constructs a verified STARTTLS SMTP sender,
wraps it with the existing optional Asynq enqueue capability, and registers
mail.send.v1. Sending opens a connection; startup does not prove SMTP delivery.
Set username and password together. SMTP and brand settings always validate;
rendering and previews do not open a connection.
Send is immediate; Enqueue stores a rendered snapshot in Redis. Both reject caller
SQL transactions. OTP mail stays immediate. For transactional multi-channel intent,
use Notifier.Enqueue. Replace example addresses, artwork and branding before
real delivery. See mail and templates.
Identity authentication#
| Key | Default |
|---|---|
AUTH_GOOGLE_ENABLED |
false |
AUTH_GOOGLE_CLIENT_ID |
unset |
AUTH_GOOGLE_TRUSTED_PRESENTER_IDS |
unset |
AUTH_ENABLED |
false |
AUTH_ACCESS_TOKEN_KEY |
unset |
AUTH_TOKEN_ISSUER |
go-modular |
AUTH_TOKEN_AUDIENCE |
go-modular-api |
AUTH_ACCESS_TTL |
168h |
AUTH_REFRESH_TTL |
0 |
AUTH_SESSION_TTL |
0 |
AUTH_PHONE_VERIFICATION_ENABLED |
false |
AUTH_VERIFICATION_KEY |
unset |
AUTH_ENABLED enables the single email OTP signup/sign-in flow, refresh
and session endpoints. It requires PostgreSQL, configured mail, both keys and the
shared Redis limiter. Sending a code returns 202; confirming it returns 201 for a
new account or 200 for an existing account, with the user and profile-completion state.
Phone verification is not implemented; enabling it fails validation.
The two keys must be independently generated, different base64-encoded 32-byte values. Store them as secrets and share them across replicas. Access signing-key replacement invalidates existing access tokens; overlapping keys are not supported. Verification-key replacement invalidates outstanding challenges and changes abuse budget keys. Never use example placeholders as credentials.
Access lifetime must be 1 minute through 7 days (default: 168h). Zero refresh/session lifetime
means no time-based expiry for that limit; rotation, logout and revocation still
apply. Positive refresh/session bounds are 30/90 days respectively, with access
no longer than either finite bound and refresh no longer than a finite session.
Use 168h refresh and 720h session for seven-day inactivity and a 30-day absolute
limit. Identity's SessionPolicy owns validation. Concurrent sessions are controlled
by domain.AllowConcurrentSessions in Go.
Google also requires authentication enabled and the configured client ID as token
audience. Additional trusted presenter IDs authorize azp, not additional audiences.
Provider linking is not implemented; matching an existing email returns a conflict.
See Identity for exact routes and account behavior.
Rate limits#
| Key | Default |
|---|---|
API_RATE_LIMIT_ENABLED |
false |
RATE_LIMIT_REDIS_URL |
redis://127.0.0.1:6379/2 |
RATE_LIMIT_NAMESPACE |
go-modular |
API_RATE_LIMIT_REQUESTS |
120 |
API_RATE_LIMIT_BURST |
120 |
API_RATE_LIMIT_PERIOD |
1m |
The broad /api/v1 limiter is opt-in. Identity still requires its narrower service
limits whenever auth or email verification is enabled. Redis failures fail closed;
limits are not bypassed. Namespace must match across replicas. Counts describe a
burst and gradual replenishment over the period. Health routes are exempt.
See Identity rate limits.
Notifications and push#
| Key | Default |
|---|---|
NOTIFICATION_DISPATCH_INTERVAL |
1s |
PUSH_ENABLED |
false |
PUSH_EXPO_PROJECT_ID |
unset |
PUSH_EXPO_ACCESS_TOKEN |
unset |
Notification dispatch runs in worker processes; zero interval selects one second, and a positive interval must be at least 100ms. Persistent delivery requires PostgreSQL, Redis and a worker; adding email or push requires the corresponding sender. Dispatch is independent of the cron scheduler.
Push is opt-in and uses Expo. A project UUID is required when enabled; the access token is required by Expo when enhanced push security is enabled. Keep provider credentials only on the server. Receipt polling uses the existing dispatcher and worker, not a separate container. See notifications and push for subscriptions, receipts and limits.
Optional tracing and metrics#
| Key | Default |
|---|---|
TRACING_TRUST_INCOMING |
false |
TRACING_ENABLED |
false |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT |
http://localhost:4318/v1/traces |
TRACING_SAMPLE_RATIO |
1 |
TRACING_EXPORT_TIMEOUT |
5s |
TRACING_SHUTDOWN_TIMEOUT |
3s |
METRICS_ENABLED |
false |
METRICS_ADDRESS |
127.0.0.1:9090 |
Tracing and metrics are independent. The trace endpoint is a complete OTLP HTTP
traces URL. In containers, localhost means that container; select a reachable
collector. Trust incoming propagation only at an intentional trusted boundary.
APP_NAME supplies both telemetry identities.
Metrics use a separate listener. Give host processes distinct addresses and keep metrics private. Tracing export failures do not fail business operations; bounded buffers may lose spans. See observability.
Filesystems#
| Key | Default |
|---|---|
FILESYSTEM_ADDITIONAL_DISKS |
unset |
FILESYSTEM_DISK |
minio |
FILESYSTEM_DISK selects a named definition from config/filesystems.go, not a
driver. disabled omits the default disk. FILESYSTEM_ADDITIONAL_DISKS explicitly
opens other configured names even when the default is disabled. Unselected disks
stay unopened. Definitions are local, s3, minio, spaces, gcs and azure; MinIO and
Spaces use the S3 driver. Adding a named disk in Go exposes its prefixed fields.
For each definition, substitute its uppercase name for <NAME> below. The selected
driver uses only relevant fields: local uses ROOT; S3 uses BUCKET, REGION, ENDPOINT,
USE_PATH_STYLE and its key fields; GCS uses BUCKET and CREDENTIALS_FILE; Azure uses
BUCKET, ENDPOINT, ACCOUNT_NAME and ACCOUNT_KEY. Other fields are ignored.
| Key | Default |
|---|---|
FILESYSTEM_<NAME>_VISIBILITY |
public |
FILESYSTEM_<NAME>_URL_MODE |
internal |
FILESYSTEM_<NAME>_PUBLIC_URL |
unset |
FILESYSTEM_<NAME>_ROOT |
storage/app |
FILESYSTEM_<NAME>_BUCKET |
unset |
FILESYSTEM_<NAME>_REGION |
us-east-1 |
FILESYSTEM_<NAME>_ENDPOINT |
unset |
FILESYSTEM_<NAME>_USE_PATH_STYLE |
false |
FILESYSTEM_<NAME>_ACCESS_KEY_ID |
unset |
FILESYSTEM_<NAME>_SECRET_ACCESS_KEY |
unset |
FILESYSTEM_<NAME>_SESSION_TOKEN |
unset |
FILESYSTEM_<NAME>_CREDENTIALS_FILE |
unset |
FILESYSTEM_<NAME>_ACCOUNT_NAME |
unset |
FILESYSTEM_<NAME>_ACCOUNT_KEY |
unset |
MinIO overrides the generic defaults with these development fixtures:
| Key | Default |
|---|---|
FILESYSTEM_MINIO_BUCKET |
uploads |
FILESYSTEM_MINIO_ENDPOINT |
http://127.0.0.1:9000 |
FILESYSTEM_MINIO_USE_PATH_STYLE |
true |
FILESYSTEM_MINIO_ACCESS_KEY_ID |
demo |
FILESYSTEM_MINIO_SECRET_ACCESS_KEY |
local-demo-minio |
S3, GCS and Azure support their native credential chains when explicit credentials
are absent. Mount credential files read-only. Cloud buckets/containers must already
exist; Compose/Tilt storage setup creates the development MinIO bucket. Host MinIO
uses the address above; containers use http://minio:9000.
Local storage needs a writable root and deliberate shared storage for multiple replicas. Changing a disk name or location does not migrate existing assets. See filesystems and Media.
Diagnose configuration failures#
Start with the selected file, existing process overrides and the relevant section above. Avoid dumping an entire environment or connection URL into a bug report. CLI help also constructs the application and can fail on unavailable resources. Use the troubleshooting guide to locate the failing stage and choose a recovery action.
Media URLs#
| Variable | Default |
|---|---|
MEDIA_BASE_URL |
http://localhost:8080/api/v1/media |
MEDIA_SIGNING_KEY |
unset |
MEDIA_BASE_URL is the browser-reachable media route prefix, not a bucket URL.
Set it to your HTTPS API origin and /api/v1/media in production. An absolute
path is also supported for same-origin clients. Never derive it from request headers.
MEDIA_SIGNING_KEY is an optional independent base64-encoded 32-byte secret shared
across replicas. It enables temporary links; without it, public URLs and authenticated
private downloads still work. Rotation invalidates existing temporary links.
Check a configuration change#
After editing a profile, restart the affected process and verify the capability's public boundary: a readiness probe, representative request, queue inspection, or provider test appropriate to the change. Environment values do not hot-reload.
Do not print a full decoded config to diagnose it. DSNs, provider tokens, signing keys, and SMTP credentials require private handling even when some structs omit them from JSON. CLI help constructs resources too; it is not a connection-free validation command.
When adding or renaming a setting in code, update this reference and its focused check:
go test ./internal/app/config -run TestConfigurationReferenceMatchesDefaults
Then verify the examples, Compose interpolation, and Kubernetes ConfigMap/Secret wiring that consume it. An environment key listed in a document but no longer read by the code is ignored; there is no automatic alias or migration layer.