# Configuration

Source: https://gortexa.sheng.page/en/configuration/

Configuration lives in `etc/config.yaml` by default; if the file is absent, that layer is skipped and built-in defaults plus environment variables apply. Loading is otherwise fail-loud: a mistyped field or a non-compliant secret fails startup instead of running degraded.

## How configuration loads

Configuration is three layers, each overriding the previous:

1. Built-in defaults (in code; e.g. `server.addr` is `:8080`, `cache.driver` is `memory`).
2. `etc/config.yaml` — point elsewhere with `GORTEXA_CONFIG=/path/to/config.yaml`; the layer is skipped when the file is absent. `GORTEXA_CONFIG` and the skip are done by the server's `main.go` (`cmd/server`), not by the `config` package: code that calls `config.WithConfigFile` itself fails on a missing file.
3. Environment variables, in the form `GORTEXA_<SECTION>__<FIELD>` (double underscore between section and field).

When the same field is set in more than one layer, the environment variable wins. Real secrets are injected only through environment variables — never written into `config.yaml`, never committed.

## config.yaml

```yaml
server:
  addr: ":8080"
  shutdown_timeout: 20s
  enable_cors: true
  cors_origins: []           # CORS allowlist; enable_cors is only the master switch — origins not listed here get no CORS headers
  openapi: true
  reflection: false          # gRPC reflection off by default

auth:
  jwt_secret: "dev-only-insecure-secret-change-me-please"
  issuer: "gortexa"
  # audience: "myapp"        # optional: stamps aud into signed tokens and requires it — isolates services sharing a secret
  ttl: 1h

db:
  dsn: ""                    # PostgreSQL DSN (Secret, masked in logs); inject via env: GORTEXA_DB__DSN
  max_conns: 10

cache:
  driver: "memory"           # "memory" (default, process-local) or "redis"

mq:
  driver: "nats"             # "nats" (core, at-most-once) or "jetstream" (durable, at-least-once)
  url: ""                    # comma-separated servers, e.g. "nats://a:4222,nats://b:4222"
  group_id: ""               # empty = fanout; non-empty = load-balance

log:
  level: "info"
  format: "json"

observ:
  service_name: "gortexa"
  service_version: "dev"
  # tracing_otlp: "localhost:4317"   # OTLP trace endpoint; empty disables
  # metrics_otlp: "localhost:4317"   # OTLP metrics endpoint; empty disables (rate-limit, circuit-breaker and health metrics need it)
  # logs_otlp: "localhost:4317"      # OTLP log endpoint; empty disables
  otlp_insecure: true                # local docker compose only; never true for remote
  sample_ratio: 1.0
  genai_capture_content: false       # when true, MCP tool-call arguments are recorded in telemetry
  # genai_mask_fields: ["password", "token", "secret", "authorization", "api_key"]
```

`genai_capture_content: true` sends AI tool-call arguments into telemetry; the default mask list only covers common field names. Combined with `otlp_insecure: true`, that content leaves the host in cleartext — do not enable both in production.

## Cache: memory and redis

`cache.driver` selects the backend. The default `memory` needs no external service; `redis` is the only driver that reads `addr` and the fields below it. For any value other than `memory` or `redis`, `cache.New` returns `cache: unsupported driver`. The server does not build a cache itself, so a mistyped driver surfaces only where your code calls `cache.New`, not at startup.

#### memory (default)
```yaml
cache:
  driver: "memory"
```

Process-local, no external dependency. Good for single-instance deployments and local development.
#### redis
```yaml
cache:
  driver: "redis"
  addr: "localhost:6379"
  password: ""            # inject via env: GORTEXA_CACHE__PASSWORD
  db: 0

  # The client tunables below are all optional; omit to use the built-in default
  dial_timeout: 5s        # default 5s
  read_timeout: 3s        # default 3s
  write_timeout: 3s       # default 3s
  pool_size: 10           # default 10; must not be negative
```

On redis, the framework's built-in zero-dependency RESP client connects — no third-party redis package.

Each tunable's zero value means "use the built-in default", so set only the one you need to change:

| Field | Default | Purpose |
| --- | --- | --- |
| `dial_timeout` | 5s | Timeout for establishing a connection |
| `read_timeout` | 3s | Timeout for reading a single reply |
| `write_timeout` | 3s | Timeout for writing a single command |
| `pool_size` | 10 | Connection pool size; `0` uses the default, negative is rejected |

Behind a load balancer, shortening the timeouts fails fast instead of stalling the whole request; a busy service can raise `pool_size`. A caller's context cancellation aborts the in-flight command (no waiting out `read_timeout`), and a command that fails on a connection reused from the idle pool is retried once on a fresh one — an LB/NAT reaping an idle flow, a Redis `timeout` or a failover no longer surfaces as a spurious first-command error; timeouts are deliberately not retried.

## Database (PostgreSQL)

`db.dsn` is the pgx connection string (a Secret field, always masked in logs); `db.max_conns` defaults to 10. Inject the real DSN via an environment variable:

```bash
GORTEXA_DB__DSN=postgres://user:pass@pgbouncer:6432/app
```

The pool is PgBouncer-safe: `QueryExecModeExec` with statement and description caches disabled, so it works behind PgBouncer in transaction-pooling mode without prepared-statement errors.

The development flow: schema goes in `db/migrations/`, SQL in `db/queries/`, and `make sqlc` generates type-safe query code into `internal/storage/db/`. `deploy/docker-compose.yaml` already ships postgres and PgBouncer for local verification.

## Environment overrides

Any field can be overridden with an environment variable, using `GORTEXA_<SECTION>__<FIELD>`:

```bash
GORTEXA_SERVER__ADDR=:9090
GORTEXA_AUTH__JWT_SECRET=<real-secret>
GORTEXA_CACHE__DRIVER=redis
GORTEXA_CACHE__ADDR=redis.internal:6379
GORTEXA_SERVER__REFLECTION=true
```

## Secret rules

- The placeholder `dev-only-insecure-secret-change-me-please` is refused at startup — it cannot ship by accident.
- JWT secrets shorter than 32 bytes are also refused.
- Services sharing a secret and the default issuer accept each other's tokens — set a distinct `auth.audience` per service to isolate them (a token minted for A is rejected by B).
- Secret fields such as the redis password and the MQ URL (which can embed credentials) are masked in logs and error output.
- Real secrets are injected via environment variables, never committed.

## MQ semantics

`mq.group_id` selects the delivery mode, identically on both drivers:

| group_id | NATS (core) | JetStream | Semantics |
| --- | --- | --- | --- |
| Empty (default) | Subscribe | Ephemeral consumer | Fanout: every subscriber receives each message |
| Non-empty | QueueSubscribe | Durable consumer (named after group_id, acked only after the handler succeeds) | Load-balance: one handler per message |

The drivers differ in the delivery guarantee: core NATS is at-most-once (no redelivery — a message whose handler fails is gone), while `driver: "jetstream"` is at-least-once — publish blocks on the server's storage ack and a handler error triggers a delayed redelivery, so handlers must be idempotent. JetStream needs the server started with `-js`; the framework lazily creates a stream per topic (24h retention), and an operator may pre-create a stream with different retention — the framework adopts it and never rewrites an existing stream.

Guarantees shared by both drivers: the header names `gortexa-key` and the whole case-insensitive `nats-*` namespace (`Nats-Msg-Id`, …) are reserved and rejected at Publish with `InvalidArgument` — on JetStream the broker acts on them (server-side de-duplication, for one, silently drops the message); a panic inside a handler is recovered into an `mq: handler panic` error that takes the same Nak/redelivery path as a returned error instead of crashing the process; if a stream disappears out of band (`nats stream rm`, a cluster rebuild), the next Publish/Subscribe re-runs the ensure step and recreates it rather than failing until a restart; the connection reconnects to the broker **indefinitely** (no ~120 s give-up as in the SDK default), with disconnect/reconnect/closed/async-error events logged through the default `slog` logger; and `Close(ctx)` waits for watcher goroutines and in-flight handlers before closing the connection, so a handler still running at shutdown can still Publish.

## Diagnostics

```bash
gortexa doctor    # check the Go toolchain and proto tools
```

## Related

- [Deployment](https://gortexa.sheng.page/en/deployment/): the three things production leaves to you
- [Batteries](https://gortexa.sheng.page/en/components/batteries/): the packages behind these settings
