# Configuration
shithub uses a layered configuration loader. Sources, in increasing-precedence order:
1. **Built-in defaults.** Encoded in `internal/infra/config/Defaults()`.
2. **TOML file.** Path comes from `SHITHUB_CONFIG` env var, falling back to `/etc/shithub/config.toml`. Absence is fine; bad syntax is a hard error.
3. **Environment variables.** `SHITHUB___` (double-underscore separates nested keys). Examples below.
4. **CLI flag overrides.** Passed in by the caller (mostly `--addr` from `shithubd web`).
After all four merge, `config.Load` applies a small set of named **aliases** (e.g. `SHITHUB_DATABASE_URL` → `db.url`) for backward compatibility, then runs `Validate`. Any validation failure causes `shithubd` to exit non-zero with a one-line error pointing at the offending key.
## Inspecting the active configuration
```sh
shithubd config print # writes the resolved config as TOML, with secrets redacted
shithubd config validate # exits non-zero if the resolved config is invalid
shithubd version # includes a one-line summary of which sinks are configured
```
`config print` redacts any field whose name contains `password`, `pass`, `secret`, `key`, `token`, `dsn`, or `url` (URL fields are redacted because they often carry credentials in the userinfo component).
## Reference
| Key | Type | Default | Notes |
|---|---|---|---|
| `env` | string | `dev` | One of `dev | staging | prod`. Drives default log format and Sentry environment. |
| `web.addr` | string | `:8080` | Listen address. |
| `web.read_timeout` | duration | `30s` | Per-request read timeout. |
| `web.write_timeout` | duration | `30s` | Per-request write timeout. |
| `web.shutdown_timeout` | duration | `10s` | Graceful drain on SIGTERM. |
| `db.url` | string | `""` | Postgres DSN. Aliased by `SHITHUB_DATABASE_URL`. |
| `db.max_conns` | int | `10` | pgxpool max conns. |
| `db.min_conns` | int | `0` | pgxpool min conns. |
| `db.connect_timeout` | duration | `5s` | |
| `log.level` | string | `info` | One of `debug | info | warn | error`. |
| `log.format` | string | `text` | One of `text | json`. |
| `metrics.enabled` | bool | `true` | Mounts `/metrics`. |
| `metrics.basic_auth_user` | string | `""` | When set together with `pass`, gate `/metrics` behind HTTP Basic. |
| `metrics.basic_auth_pass` | string | `""` | |
| `tracing.enabled` | bool | `false` | When true, `tracing.endpoint` is required. |
| `tracing.endpoint` | string | `""` | OTLP HTTP endpoint, e.g. `http://otel-collector:4318`. |
| `tracing.sample_rate` | float | `0.05` | Parent-based ratio sampler in [0, 1]. |
| `tracing.service_name` | string | `shithubd` | OTel resource attribute. |
| `error_reporting.dsn` | string | `""` | Sentry-protocol DSN (works against GlitchTip). Empty disables. |
| `error_reporting.environment` | string | `""` | Tag for filtering events. |
| `error_reporting.release` | string | `""` | Tag for filtering events. |
| `session.key_b64` | string | `""` | Base64 32-byte AEAD key. Aliased by `SHITHUB_SESSION_KEY`. |
| `session.max_age` | duration | `720h` | Cookie session lifetime (30 days). |
| `session.secure` | bool | `false` | Set `Secure` cookie attribute. Enable under TLS (S37 deploy). |
| `storage.repos_root` | string | `/data/repos` | Filesystem root for bare repos. Required. |
| `storage.s3.endpoint` | string | `""` | S3-compatible endpoint host[:port], no scheme. Empty disables S3. |
| `storage.s3.region` | string | `us-east-1` | Region for SigV4 signing. |
| `storage.s3.access_key_id` | string | `""` | |
| `storage.s3.secret_access_key` | string | `""` | Redacted by `config print`. |
| `storage.s3.bucket` | string | `""` | Single bucket per environment. |
| `storage.s3.use_ssl` | bool | `false` | True for Spaces, false for local MinIO. |
| `storage.s3.force_path_style` | bool | `true` | True for MinIO, false for Spaces. |
| `auth.require_email_verification` | bool | `true` | When true, login is rejected until the primary email is verified. |
| `auth.base_url` | string | `http://127.0.0.1:8080` | Used for absolute links in transactional emails. |
| `auth.site_name` | string | `shithub` | Branding token for email subjects/bodies. |
| `auth.email_from` | string | `shithub ` | Envelope From for outgoing email. |
| `auth.email_backend` | string | `stdout` | One of `stdout | smtp | postmark | resend`. |
| `auth.smtp.addr` | string | `127.0.0.1:1025` | Required when `email_backend=smtp`. |
| `auth.smtp.username` | string | `""` | Optional SMTP auth username. |
| `auth.smtp.password` | string | `""` | Optional SMTP auth password. Redacted by `config print`. |
| `auth.postmark.server_token` | string | `""` | Required when `email_backend=postmark`. Redacted. |
| `auth.resend.api_key` | string | `""` | Required when `email_backend=resend`. Redacted. |
| `auth.argon2.memory_kib` | uint32 | `65536` | argon2id memory cost (KiB). |
| `auth.argon2.time` | uint32 | `3` | argon2id iterations. |
| `auth.argon2.threads` | uint8 | `2` | argon2id parallelism. |
| `auth.totp_key_b64` | string | `""` | Base64 32-byte AEAD key for at-rest TOTP secrets. Aliased by `SHITHUB_TOTP_KEY`. Empty disables 2FA enrollment routes. |
| `billing.enabled` | bool | `false` | Enables paid-organization Stripe Billing flows. When false, org plan state is local-only. |
| `billing.grace_period` | duration | `336h` | Lock grace window applied after failed subscription payments. |
| `billing.stripe.secret_key` | string | `""` | Stripe secret API key. Required when `billing.enabled=true`. Redacted. |
| `billing.stripe.webhook_secret` | string | `""` | Stripe webhook signing secret. Required when `billing.enabled=true`. Redacted. |
| `billing.stripe.team_price_id` | string | `""` | Stripe recurring Price ID for the Team plan seat. Required when `billing.enabled=true`. |
| `billing.stripe.success_url` | string | `""` | Optional absolute Checkout success URL override. Empty derives from `auth.base_url`. Redacted by `config print`. |
| `billing.stripe.cancel_url` | string | `""` | Optional absolute Checkout cancel URL override. Empty derives from `auth.base_url`. Redacted by `config print`. |
| `billing.stripe.portal_return_url` | string | `""` | Optional absolute Billing Portal return URL override. Empty derives from `auth.base_url`. Redacted by `config print`. |
| `billing.stripe.automatic_tax` | bool | `false` | Enables Stripe Checkout automatic tax collection when the Stripe account is configured for it. |
## Env-var examples
```sh
# Listen elsewhere
export SHITHUB_WEB__ADDR=:9090
# Connect to Postgres
export SHITHUB_DATABASE_URL=postgres://shithub:dev@127.0.0.1:5432/shithub?sslmode=disable
# (equivalent: export SHITHUB_DB__URL=...)
# JSON logs for prod
export SHITHUB_LOG__FORMAT=json
export SHITHUB_LOG__LEVEL=info
# Enable tracing
export SHITHUB_TRACING__ENABLED=true
export SHITHUB_TRACING__ENDPOINT=http://otel-collector.bare-metal:4318
export SHITHUB_TRACING__SAMPLE_RATE=0.05
# Error reporting via GlitchTip
export SHITHUB_ERROR_REPORTING__DSN=https://glitchtip.bare-metal/
# Session signing key (deterministic across restarts in prod)
export SHITHUB_SESSION_KEY=$(openssl rand -base64 32)
# Gate /metrics behind Basic auth
export SHITHUB_METRICS__BASIC_AUTH_USER=prom
export SHITHUB_METRICS__BASIC_AUTH_PASS=
# Enable Stripe Billing in test mode
export SHITHUB_BILLING__ENABLED=true
export SHITHUB_BILLING__STRIPE__SECRET_KEY=sk_test_...
export SHITHUB_BILLING__STRIPE__WEBHOOK_SECRET=whsec_...
export SHITHUB_BILLING__STRIPE__TEAM_PRICE_ID=price_...
```
## Secrets
- **Never** commit secrets. `.env` is gitignored; production keys live in a systemd `EnvironmentFile=` with mode `0600`.
- The redaction behavior of `config print` is documented above and tested in `internal/infra/config/config_test.go`. If you add a new secret-bearing field, name it so the redactor matches it (containing `pass`, `secret`, `key`, `token`, `dsn`, `password`, or `url`) — or extend `secretFieldNames` in `internal/infra/config/redact.go`.
- Log-line redaction is independent (see `docs/internal/observability.md`). Both layers exist on purpose; secrets in env-loaded config and in handler-emitted logs travel different paths.
## Adding a new key
1. Add the field to the appropriate config struct in `internal/infra/config/config.go` with a `toml:` tag.
2. Set its default in `Defaults()`.
3. Add validation in `Validate()` if it has invariants.
4. If it's secret-bearing, confirm its name matches the redactor.
5. Document it in this file.
6. Update `.env.example` if the env-var form is the typical usage.