Skip to content

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.

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.
  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.

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 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"]

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. Any value other than memory or redis fails startup with cache: unsupported driver.

cache:
driver: "memory"

Process-local, no external dependency. Good for single-instance deployments and local development.

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

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:

Terminal window
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.

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

Terminal window
GORTEXA_SERVER__ADDR=:9090
GORTEXA_AUTH__JWT_SECRET=<real-secret>
GORTEXA_CACHE__DRIVER=redis
GORTEXA_CACHE__ADDR=redis.internal:6379
GORTEXA_SERVER__REFLECTION=true
  • 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.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.

Terminal window
gortexa doctor # check the Go toolchain and proto tools