Skip to content
latest
Getting Started / latest

Configuration

Configuration is application code in the config package. It reads the process environment into typed structs, validates each one at startup, and fails fast with an error that names the section.

How configuration loads

Each process entry loads .env and then builds the configuration it needs:

1// cmd/app/main.go
2if err := env.Load(); err != nil {
3    return err
4}
5
6cfg, err := config.New()
7if err != nil {
8    return err
9}
  • env.Load() reads KEY=VALUE pairs from .env and sets them in the process environment. Keys already present in the process environment win, so a deployment secret overrides the file. A missing .env is not an error; required keys then have to come from the real environment.
  • config.New() calls one constructor per section (NewApp, NewHTTP, NewAuth, NewSession, NewCache, NewDatabase, NewQueueInsert, NewQueueWorker, NewTelemetry, NewMail, NewInertia) and returns one config.Config value.
  • Every constructor reads with pkg/env, validates, and wraps its failures as config: <section>: .... Malformed values are startup errors, not request-time surprises.
  • cmd/migrate and cmd/seeds call only config.NewDatabase(). They do not need HTTP, session, mail or Inertia settings.
  • Nothing outside config/ reads environment variables. internal/runtime receives typed values and passes them down.

Application and HTTP

Variable Default Meaning
ENVIRONMENT development Runtime policy; production enables secure cookies, production logging and the Mailpit guard
PROJECT_NAME scaffold project name Application identity and the fallback telemetry name
SITE_NAME Andurel Display name shared with Inertia pages
HOST_PRIMARY required Public hostname, optionally with a port; the primary route host
PROTOCOL http when empty Public scheme used to build URLs
HOST localhost Listen address
PORT 8080 Listen port, 1 through 65535
HTTP_IDLE_TIMEOUT 120s Keep-alive idle bound; zero disables it
HTTP_READ_TIMEOUT 10s Request-read bound; zero disables it
HTTP_WRITE_TIMEOUT 30s Response-write bound; zero disables it
CORS_ALLOWED_ORIGINS empty Comma-separated additional credentialed CORS origins
CSRF_STRATEGY header_only Or header_or_legacy_token for form-token clients
CSRF_TRUSTED_ORIGINS empty Additional comma-separated trusted origins
HTTP_TRUSTED_PROXIES empty IPs or CIDRs of reverse proxies whose X-Forwarded-For is believed

Configured host origins (PROTOCOL + HOST_PRIMARY and any named hosts) are trusted for CORS and CSRF automatically. Behind a proxy, a normal split is HOST_PRIMARY=app.example.com, PROTOCOL=https, HOST=0.0.0.0, and an internal PORT.

Database

Variable Default Meaning
DB_KIND postgres Database engine name
DB_HOST / DB_PORT 127.0.0.1 / 5432 PostgreSQL address
DB_NAME project database Database name
DB_USER / DB_PASSWORD postgres / postgres Credentials
DB_SSL_MODE disable pgx SSL mode, up to verify-full
DB_APPLICATION_NAME project database PostgreSQL activity identity
DB_CONNECT_TIMEOUT 5s Initial connection timeout
DB_STATEMENT_CACHE_CAPACITY 512 Statement-cache entries; zero disables caching
DB_DESCRIPTION_CACHE_CAPACITY 512 Description-cache entries; zero disables caching
DB_MAX_OPEN_CONNECTIONS 25 Open pool limit
DB_MAX_IDLE_CONNECTIONS 25 Idle pool limit
DB_CONNECTION_MAX_LIFETIME 1h Reusable connection lifetime
DB_CONNECTION_MAX_IDLE_TIME 30m Idle connection lifetime
DB_OPEN_TELEMETRY true pgx instrumentation

Budget connections across every web and queue replica, leaving room for migrations and administration. See Database.

Sessions and authentication

Variable Default Meaning
SESSION_KEY required Hexadecimal cookie authentication key
SESSION_ENCRYPTION_KEY required Hexadecimal cookie encryption key
SESSION_MAX_AGE 604800 Session lifetime in seconds; must be positive
TOKEN_SIGNING_KEY required Account-token signing key
PEPPER required Current password pepper
PREVIOUS_PEPPERS empty Comma-separated peppers accepted during a rotation window

Production values belong in deployment secrets, never in a committed .env. The cookie name is derived as app_sess_<project-slug>-<environment>, so environments on the same hostname do not share sessions. Rotating the session keys invalidates live cookies; rotating PEPPER moves the old value into PREVIOUS_PEPPERS until users have signed in again. See Authentication.

Cache

Variable Default Meaning
CACHE_DRIVER memory memory or redis
CACHE_NAMESPACE project name Key prefix shared by replicas
CACHE_REDIS_ADDR empty Required when the driver is redis

Rate-limit counters live in this store, so a multi-replica deployment needs the Redis driver for shared limits.

Queue

Both queue configs start from storage.DefaultQueueConfig(). Insertion reads QUEUE_MAX_ATTEMPTS and QUEUE_SCHEMA; the worker clones that base and adds processing policy:

Variable Default Meaning
QUEUE_MAX_WORKERS 100 Concurrency for the default queue
QUEUE_ADVISORY_LOCK_PREFIX 0 River lock namespace
QUEUE_CANCELLED_JOB_RETENTION_PERIOD 24h Cancelled-job retention
QUEUE_COMPLETED_JOB_RETENTION_PERIOD 24h Completed-job retention
QUEUE_DISCARDED_JOB_RETENTION_PERIOD 168h Discarded-job retention
QUEUE_FETCH_COOLDOWN / QUEUE_POLL_INTERVAL River defaults Fetch timing; polling cannot be shorter than the cooldown
QUEUE_ID empty Optional client identifier
QUEUE_JOB_CLEANER_TIMEOUT 30s Job-cleaner timeout
QUEUE_JOB_STUCK_THRESHOLD / QUEUE_JOB_TIMEOUT River defaults Stuck-job and execution bounds
QUEUE_REINDEXER_TIMEOUT 1m Reindex bound
QUEUE_RESCUE_STUCK_JOBS_AFTER 1h Must not be shorter than the job timeout
QUEUE_SOFT_STOP_TIMEOUT River default Shutdown grace period
QUEUE_POLL_ONLY false Disable notification-assisted fetching
QUEUE_SKIP_JOB_KIND_VALIDATION false Skip registered-kind validation
QUEUE_SKIP_UNKNOWN_JOB_CHECK false Skip the stored-kind check

Named queues and per-queue concurrency are code, not environment: edit NewQueueWorker in config/queue.go. See Queues.

Email and telemetry

DEFAULT_SENDER_SIGNATURE defaults to noreply@ plus the primary host, with any listen port removed. EMAIL_PROVIDER selects the transport:

  • mailpit (the default) sends to Mailpit on MAILPIT_HOST / MAILPIT_PORT (0.0.0.0 / 1025) and is refused in production.
  • smtp uses SMTP_HOST, SMTP_PORT (default 587), SMTP_USERNAME, SMTP_PASSWORD, and SMTP_TLS (starttls, tls, or none).
Telemetry variable Default
LOG_LEVEL INFO
TELEMETRY_SERVICE_NAME slugged project name
TELEMETRY_SERVICE_VERSION 1.0.0
OTLP_LOGS_ENDPOINT, OTLP_METRICS_ENDPOINT, OTLP_TRACES_ENDPOINT empty
OTLP_HEADERS empty
TRACE_SAMPLE_RATE 1.0, valid from 0 through 1
TELEMETRY_BATCH_SIZE 512
TELEMETRY_BATCH_TIMEOUT_MS 5000

Inertia and SSR

Variable Default Meaning
INERTIA_CONTAINER_ID app DOM element that mounts the page
INERTIA_ENTRY_POINT resources/js/app.tsx Vite entry point
INERTIA_VITE_DEV_URL http://localhost:5173/assets/dist Development asset base
INERTIA_PROTOCOL_DEBUG true Log Inertia request classification and metadata keys
INERTIA_SSR_RUNTIME node Managed SSR executable
INERTIA_SSR_BUNDLE assets/dist/ssr/ssr.js Managed SSR bundle
INERTIA_SSR_LISTEN http://127.0.0.1:13714 Address the SSR process binds
INERTIA_SSR_URL http://127.0.0.1:13714 Address the app POSTs /render to
INERTIA_SSR_STARTUP_TIMEOUT 10s Managed process health deadline
INERTIA_SSR_REQUEST_TIMEOUT 2s Render deadline
INERTIA_SSR_MAX_RESPONSE_BYTES 2097152 Render response-size bound
INERTIA_SSR_MINIMUM_MAJOR 22 Oldest accepted Node.js major for managed SSR
INERTIA_SSR_FAIL_FAST true Fail the page instead of falling back when SSR is unavailable

SSR is opted into per page, not switched on globally. The shipped .env.example sets INERTIA_SSR_FAIL_FAST=false for local development, where no SSR process runs; a production deployment should decide the value explicitly. See Views.

Adding an application setting

Add a focused type, a constructor that reads and validates it, a field on config.Config, and a call from config.New:

 1type Search struct {
 2    Endpoint string
 3    Timeout  time.Duration
 4}
 5
 6func NewSearch() (Search, error) {
 7    e := env.New()
 8    cfg := Search{
 9        Endpoint: e.String("SEARCH_ENDPOINT", ""),
10        Timeout:  e.Duration("SEARCH_TIMEOUT", 2*time.Second),
11    }
12    if err := e.Err(); err != nil {
13        return Search{}, fmt.Errorf("config: search: %w", err)
14    }
15    if cfg.Endpoint == "" {
16        return Search{}, fmt.Errorf("config: search: SEARCH_ENDPOINT is required")
17    }
18    if cfg.Timeout <= 0 {
19        return Search{}, fmt.Errorf("config: search: SEARCH_TIMEOUT must be positive")
20    }
21
22    return cfg, nil
23}

For a set of related fields, collect failures with validation.NewBuilder() and return b.Err() instead of testing each one by hand. Then construct the value in config.New, store it on config.Config, and pass it to the one dependency that needs it in internal/runtime. Copy maps or slices before another process mutates them; storage.QueueConfig.Clone is the pattern to follow.

Keep startup errors fatal. Continuing with a partial default turns a clear deployment failure into request-time corruption.