Skip to content

Configuration reference

PgQueryNarrative is configured entirely by environment variables; there is no config file. Every variable below is read in internal/config/config.go (or, where noted, outside Load()); this page is checked against that file by make docs-contract-check on every change. Boolean variables use Go's strconv.ParseBool: 1/t/T/TRUE/true/True and 0/f/F/FALSE/false/False only, not yes/on. An unparseable or empty value silently falls back to the default for every variable type.

Production StrictMode (APP_ENV=production/prod, or SECURITY_STRICT=true) enforces a large additional set of restrictions, listed together in Production configuration rather than repeated per row here; this page states each variable's ordinary default and behavior.

Loading config

Method Usage
Env export PGQUERYNARRATIVE_PORT=8081 then start
.env Create .env in the project root (gitignored); export $(cat .env | xargs) before starting. Never commit secrets
Docker Compose Set environment under app in docker-compose.yml

Logging

Variable Default Description
LOG_LEVEL info debug, info, warn, error (zerolog)
LOG_PRETTY unset → pretty Unset or truthy = human-readable colorized logs; false/0 = one JSON line per log
LOG_DEBUG empty 1/true (parsed by a separate, more permissive helper) = extra-verbose logging of query execution and report generation, independent of LOG_LEVEL

Server

Variable Default Description
PGQUERYNARRATIVE_HOST 0.0.0.0 Bind address
PGQUERYNARRATIVE_PORT 8080 Server port
PGQUERYNARRATIVE_READ_TIMEOUT 15s Request read timeout
PGQUERYNARRATIVE_WRITE_TIMEOUT 300s Response write timeout (Ask/report chains several LLM calls)
SHUTDOWN_TIMEOUT 10s Graceful shutdown timeout
CORS_ORIGINS empty Comma-separated allowed origins; empty = same-origin only

Database

Variable Default Description
POSTGRES_IMAGE postgres:16-alpine Base image the root Compose file builds (with HypoPG) as pgquerynarrative-postgres:hypopg, not a pulled tag. Compose-only, not read by the app
DATABASE_HOST localhost Metadata database host
DATABASE_PORT 5432 Metadata database port
DATABASE_NAME pgquerynarrative Metadata database name
DATABASE_USER pgquerynarrative_app App role: metadata reads/writes (app.*)
DATABASE_PASSWORD pgquerynarrative_app App role password. ≥16 chars and not a placeholder in production
DATABASE_READONLY_USER pgquerynarrative_readonly Default connection's read-only role
DATABASE_READONLY_PASSWORD pgquerynarrative_readonly Read-only password. Same production rule as above; enforced per connection too
DATABASE_SSL_MODE disable disable/allow/prefer/require/verify-ca/verify-full. Production requires require, verify-ca, or verify-full
DATABASE_MAX_CONNECTIONS 10 Pool size per connection
DATABASE_MIN_CONNECTIONS 0 Minimum idle connections per pool
DATABASE_GLOBAL_MAX_CONNECTIONS 0 (off) Shared connection budget across the app pool and every read-only pool combined; requests fail once exceeded
QUERY_TIMEOUT 30s Query execution timeout. Must be > 0 in production
QUERY_LOCK_TIMEOUT 2s Session lock_timeout for query sessions. Must be > 0 in production
QUERY_IDLE_IN_TX_TIMEOUT 10s Session idle_in_transaction_session_timeout. Must be > 0 in production
QUERY_MAX_RESULT_BYTES 10485760 (10 MiB) Approximate max materialized result size before QUERY_RESULT_TOO_LARGE. Must be > 0 in production
QUERY_MAX_CELL_BYTES 1048576 (1 MiB) Approximate max size for one cell. Must be > 0 in production
QUERY_MAX_COLUMNS 100 Max columns in a result. Must be > 0 in production
DATABASE_MAX_QUERY_LENGTH 10000 Max statement length in bytes the query validator accepts (1,000–1,000,000). Raise it to import PostgREST requests with large in.(...) lists
DATABASE_ALLOWED_SCHEMAS demo Comma-separated schema allowlist. Never app, pg_catalog, information_schema, pg_toast*, always rejected. Must be non-empty and exclude public in production
DATABASE_DEFAULT_CONNECTION_ID default Connection used when connection_id is omitted or blank. If this id isn't among the configured connections, the first configured connection is used instead
DATABASE_CONNECTIONS_JSON empty JSON array of additional read-only connections, see below

Migration identity (outside Load())

Read directly by the container entrypoint, not by the Go config loader; unset again before the server process starts:

Variable Default Description
DATABASE_MIGRATION_USER falls back to DATABASE_USER Role used to run migrate up at container start. Needs CREATE EXTENSION and ALTER ROLE
DATABASE_MIGRATION_PASSWORD falls back to DATABASE_PASSWORD Password for the migration role
DATABASE_MIGRATION_URL derived from the above Full migration DSN; overrides the two variables above when set
PGQUERYNARRATIVE_SKIP_MIGRATIONS false Skip running migrations at container start entirely (e.g. when a separate Job does it)
PGQUERYNARRATIVE_SEED false Load tools/db/seed.sql after migrating

Under production StrictMode, if DATABASE_MIGRATION_USER equals DATABASE_USER and no DATABASE_MIGRATION_URL is set, the entrypoint refuses to start rather than run migrations as a role that can't finish them. See Deployment: migration identity.

Multiple database connections

App tables (saved_queries, reports, audit, embeddings, …) always stay in the single metadata database configured above. DATABASE_CONNECTIONS_JSON adds analytical connections on top of the implicit default one.

Each array entry is matched to Go struct field names case-insensitively (camelCase, e.g. readOnlyUser, works; there are no JSON tags, and snake_case keys are silently dropped). Durations (queryTimeout, lockTimeout, idleTxTimeout) must be given as integer nanoseconds, not duration strings: "queryTimeout": "30s" fails to parse and, because the whole variable is parsed as one JSON value, the entire DATABASE_CONNECTIONS_JSON is ignored (only the default connection loads); check the server log for config: ignoring DATABASE_CONNECTIONS_JSON: ... if a connection you configured doesn't appear. An entry missing id, host, database or readOnlyUser is skipped individually. Unset optional fields inherit from the corresponding DATABASE_*/QUERY_* default.

Fields: id, name, host, port, database, readOnlyUser, readOnlyPassword, sslMode, queryTimeout (ns), lockTimeout (ns), idleTxTimeout (ns), allowedSchemas (array of strings), maxResultBytes, maxCellBytes, maxColumns.

export DATABASE_DEFAULT_CONNECTION_ID=default
export DATABASE_CONNECTIONS_JSON='[
  {
    "id": "staging",
    "name": "Staging",
    "host": "staging-db.internal",
    "port": 5432,
    "database": "analytics_staging",
    "readOnlyUser": "analytics_ro",
    "readOnlyPassword": "secret",
    "sslMode": "require",
    "queryTimeout": 30000000000,
    "allowedSchemas": ["analytics"]
  }
]'

Requests choose a connection with connection_id, see Multiple connections for resolution rules (unknown id fails with CONNECTION_NOT_FOUND, it does not fall back silently).

Security

Authentication

Variable Default Description
SECURITY_AUTH_ENABLED false Requires Bearer/session/OIDC on /api/*, /metrics, and the non-shared report export routes. Must be true in production
SECURITY_ALLOW_INSECURE_NO_AUTH false Required (true) when auth is off, explicit opt-in for open local/dev access. Forbidden in production
SECURITY_API_KEY empty Plaintext bearer token. ≥16 chars if set. Forbidden in production, use the hash below
SECURITY_API_KEY_HASH empty SHA-256 hex of the bearer token, compared in constant time
SECURITY_API_KEYS_JSON empty JSON array of managed keys (key_hash, id, role, scopes, expires_at, revoked). role is required, expires_at must be RFC 3339, and any mistake stops startup (details). Must use key_hash, not plaintext key, in production
SECURITY_TRUSTED_PROXIES empty Comma-separated CIDRs trusted to set forwarded-for headers for per-IP rate limiting
SECURITY_MAX_REQUEST_BODY_BYTES 5242880 (5 MiB) Hard cap on request body size

At least one of SECURITY_API_KEY, SECURITY_API_KEY_HASH, SECURITY_API_KEYS_JSON or SECURITY_OIDC_ISSUER is required whenever auth is enabled.

Rate limiting

Variable Default Description
SECURITY_RATE_LIMIT_RPM 0 (disabled) Max requests per minute. Counted per organization, identity and route class for a signed-in session or an API key or token that has authenticated (a managed key's first request is still counted by IP), and per client IP for everything else. Must be > 0 in production
SECURITY_RATE_LIMIT_BURST 0 (= 2× RPM) Burst size
SECURITY_RATE_LIMIT_DISTRIBUTED false Use the PostgreSQL-backed limiter instead of in-memory. Must be true in production when RPM > 0
SECURITY_RATE_LIMIT_FAILURE_MODE closed if StrictMode or auth is on, else open open/closed/local_fallback. Cannot be open while auth is enabled, or in production
SECURITY_RATE_LIMIT_BUCKET_MAX_AGE 24h How long an idle rate-limit bucket is retained before cleanup

Audit

Variable Default Description
SECURITY_AUDIT_MODE required under StrictMode, else best_effort best_effort/required/buffered. best_effort is forbidden in production. See Data handling

Data at rest and sessions

Variable Default Description
SECURITY_DATA_ENCRYPTION_KEY empty Seals SQL text and connection secrets at rest (AES-GCM). ≥32 chars, not a placeholder, if set. Either this or the session secret is required in production
SECURITY_SESSION_SECRET empty HMAC secret for session cookies; also usable as the encryption-key fallback. ≥32 chars, not a placeholder, if set. Required in production when browser OIDC is configured
SECURITY_SESSION_TTL 8h Browser session lifetime

Sharing

Variable Default Description
SECURITY_SHARE_LINKS_ENABLED false Allows unauthenticated report share links. Forbidden in production until sharing is explicitly hardened
SECURITY_SHARE_LINK_DEFAULT_HOURS 168 (7 days) Default share-link expiry
SECURITY_SHARE_LINK_EXPOSE_SQL false Include the underlying SQL in the shared view

Query execution

Variable Default Description
SECURITY_EXPLAIN_ANALYZE_ENABLED false Allows EXPLAIN ANALYZE, which executes the query. Forbidden in production
SECURITY_EXPLAIN_SNAPSHOT_RETENTION_DAYS 90 (0 = forever) Retention for stored EXPLAIN snapshots, cleaned up every 6h
SECURITY_STAT_STATEMENTS_ENABLED true Whether pg_stat_statements reads (query stats, regression polling) are attempted
SECURITY_CONNECTION_ALLOWLIST_REQUIRED on under StrictMode, else off When on, an organization with no assignment for a connection is denied instead of allowed by default
SECURITY_POSTGREST_REPLAY_ROLES empty Comma-separated PostgREST roles an imported request may be replayed as (SET LOCAL ROLE, so role-keyed RLS matches the endpoint). Empty never impersonates. Each role must also be granted to the analytical role. request.jwt.claims is never replayed, for any role: the capture is unverified caller-supplied text, so claims-keyed RLS (e.g. auth.uid()) cannot be reproduced

OIDC

Variable Default Description
SECURITY_OIDC_ISSUER empty Corporate IdP issuer URL. When set in production, SECURITY_OIDC_AUDIENCE is required
SECURITY_OIDC_AUDIENCE empty Expected JWT audience
SECURITY_OIDC_JWKS_URL empty JWKS endpoint override, when it differs from IdP discovery
SECURITY_OIDC_CLIENT_ID empty OAuth2/OIDC client ID
SECURITY_OIDC_CLIENT_SECRET empty OIDC client secret (omit for public clients)
SECURITY_OIDC_REDIRECT_URL http://localhost:8080/auth/callback Callback URL registered at the IdP. An empty string counts as unset and falls back to the localhost default; set this explicitly for any real deployment
SECURITY_OIDC_AUTO_JOIN_DEFAULT_ORG false Auto-provision default-org membership on first OIDC login. Forbidden (must stay false) in production

Schedules and webhooks

Variable Default Description
SCHEDULE_RUNNER_ENABLED false Enables the background schedule/webhook runner
SCHEDULE_RUNNER_INTERVAL 1m Poll interval for due schedules
SCHEDULE_DURABLE_LEASES true Must stay true when the runner is enabled in production
SECURITY_WEBHOOK_SIGNING_SECRET empty HMAC secret for X-PGQN-Signature. ≥16 chars, not a placeholder, required when the runner is enabled in production
SECURITY_WEBHOOK_ALLOWED_HOSTS empty Comma-separated webhook destination hosts. Empty fails closed: no webhook fires. Required when the runner is enabled in production

Regression poller

Variable Default Description
REGRESSION_POLLER_ENABLED = SECURITY_STAT_STATEMENTS_ENABLED Enables the background regression-detection poller
REGRESSION_POLLER_INTERVAL 15m Poll interval per (organization, connection)
REGRESSION_MEAN_THRESHOLD_PCT 50 Mean-latency increase that triggers an alert
REGRESSION_HIGH_THRESHOLD_PCT 100 Impact tier: high
REGRESSION_CRITICAL_THRESHOLD_PCT 200 Impact tier: critical
REGRESSION_SNAPSHOT_RETENTION_DAYS 7 Retention for raw statement snapshots

See Regressions and applied fixes for how these combine into an alert.

LLM

Required for Ask, chat, and plain-English SQL explanation; used with a deterministic fallback for workbench report narratives. Not used by plan findings, candidates, compare, verification, or investigation reports. See LLM providers.

Variable Default Description
LLM_PROVIDER ollama ollama|gemini|claude|openai|groq. Anything other than ollama/empty counts as a cloud provider
LLM_MODEL llama3.2 Model name
LLM_BASE_URL http://localhost:11434 LLM API base URL
LLM_API_KEY empty Required for cloud providers
LLM_ALLOW_EXTERNAL_DATA false Must be true before any cloud provider can be used at all
LLM_SEND_ROW_DATA false Include raw row samples in prompts
LLM_MAX_SAMPLE_ROWS 5 (clamped 0–10) Row sample cap; ≤3 for cloud providers when row data is sent; ≤5 in production regardless
LLM_REDACT_PII true Redacts PII patterns and SQL literals before prompting. Must stay true in production when a cloud provider is sent row data
LLM_MAX_CALLS_PER_REPORT 12 Auxiliary LLM calls allowed per report
LLM_DAILY_TOKEN_BUDGET / LLM_MONTHLY_TOKEN_BUDGET 0 (unlimited) Token budget ceilings
LLM_DAILY_COST_BUDGET_USD / LLM_MONTHLY_COST_BUDGET_USD 0 Cost budget ceilings
LLM_PER_USER_DAILY_TOKEN_BUDGET / LLM_PER_USER_MONTHLY_TOKEN_BUDGET 0 Per-user token ceilings
LLM_PER_USER_DAILY_COST_BUDGET_USD / LLM_PER_USER_MONTHLY_COST_BUDGET_USD 0 Per-user cost ceilings
LLM_USD_PER_1K_TOKENS 0.002 Rate used to convert tokens into the cost budgets above
LLM_BUDGET_FAIL_CLOSED true for a cloud provider or under StrictMode, else false Deny LLM calls (rather than allow) when the budget ledger is unreachable. Must be true for cloud providers in production

Embeddings

Used for GET /suggestions/similar, GET /reports/similar, and RAG context in report generation. When unset, saving still works but no vectors are stored.

Variable Default Description
EMBEDDING_BASE_URL empty Embedding API URL. Falls back to LLM_BASE_URL when LLM_PROVIDER=ollama
EMBEDDING_MODEL nomic-embed-text Embedding model

See Semantic search (pgvector).

MCP

Not part of Load(); the MCP server is a separate binary reading its own environment. See MCP server for the full list (PGQUERYNARRATIVE_URL, PGQUERYNARRATIVE_API_KEY).

Metrics

Analytics windows and thresholds; shown read-only in Settings → Analytics. Out-of-range values are clamped at load, never rejected.

Variable Default Range
METRICS_TREND_PERIODS 6 2–24
METRICS_MOVING_AVG_WINDOW 3 2–24
METRICS_MAX_TIMESERIES_PERIODS 24 2–120
METRICS_MAX_SEASONAL_LAG 12 2–24
METRICS_MIN_PERIODS_FOR_SEASONALITY 12 ≥ 2
PERIOD_TREND_THRESHOLD_PERCENT 0.5 any
METRICS_ANOMALY_SIGMA 2.0 1–5
METRICS_ANOMALY_METHOD zscore zscore|isolation_forest
METRICS_CONFIDENCE_LEVEL 0.95 0.5–0.99
METRICS_CORRELATION_MIN_ROWS 10 2–1000
METRICS_SMOOTHING_ALPHA 0.3 (0, 1]
METRICS_SMOOTHING_BETA 0.1 [0, 1]

Environment mode (outside Load())

Variable Default Description
APP_ENV empty production/prod (case-insensitive) enables StrictMode; demo enables DemoMode (fabricated workspace KPIs and seeded regression alerts)
SECURITY_STRICT false Alternative StrictMode switch, independent of APP_ENV

Production

See Production configuration for the complete list of StrictMode requirements; it is generated from the same validation function this page's defaults come from, so treat that page as the authoritative gate and this page as the variable dictionary.

See also

Installation · API reference · Deployment · Documentation index