# Configuration reference

Generated by `cd backend && go run ./cmd/genconfigreference` from the `cfgreg` struct tags on `config.Config` (issue #933, #501 action 1-2/7) — never hand-edited. The completeness test `config.TestConfigRegisterCoversEveryField` fails CI if a `Config` field is added without a tag, and the drift test `config.TestConfigurationReferenceDocUpToDate` fails until this file is regenerated after a tag changes.

## config.Config surface

| Variable | Type | Default | Required | Restart required | Range / enum | Description |
|---|---|---|---|---|---|---|
| `SQLITE_DB_PATH` | string | mycorrhizal.db | yes | yes | — | SQLite database file path |
| `REMINDER_TIME` | string | 06:00 | no | yes | HH:MM 24h wall time | Daily reminder wall-clock time |
| `REMINDER_TIMEZONE` | string | UTC | no | yes | IANA timezone name | Reminder clock's IANA timezone |
| `FRONTEND_URL` | string | * | no | yes | absolute origin, or * for dev only | Frontend origin used for CORS and OIDC redirect URLs |
| `PORT` | int | 8080 | no | yes | 1..65535 | HTTP listen port |
| `TRUSTED_PROXIES` | stringlist | (loopback 127.0.0.1/32, ::1/128) | no | yes | IP or CIDR, no 0.0.0.0/0 or ::/0 | Reverse-proxy addresses trusted for X-Forwarded-For |
| `*(derived: UseResend)*` | — | — | no | no | — | true when RESEND_API_KEY and RESEND_FROM_EMAIL are both set |
| `RESEND_API_KEY` | string | — | no | yes | — | Resend email API key |
| `RESEND_FROM_EMAIL` | string | — | no | yes | — | Resend sender address |
| `*(derived: UseSMTP)*` | — | — | no | no | — | true when SMTP_HOST and SMTP_FROM_EMAIL are both set |
| `SMTP_HOST` | string | — | no | yes | — | SMTP server hostname |
| `SMTP_PORT` | int | 587 | no | yes | 1..65535 when SMTP enabled | SMTP server port |
| `SMTP_USERNAME` | string | — | no | yes | — | SMTP auth username |
| `SMTP_PASSWORD` | string | — | no | yes | — | SMTP auth password |
| `SMTP_FROM_EMAIL` | string | — | no | yes | — | SMTP sender address |
| `SMTP_USE_TLS` | bool | false | no | yes | — | Use implicit TLS (e.g. port 465) instead of STARTTLS |
| `JWT_SECRET_KEY` | string | — | yes | yes | >=32 bytes, not a known placeholder, sufficient entropy | Secret key signing auth JWTs |
| `JWT_EXPIRY_HOURS` | int | 96 | no | yes | 1..8760 | JWT absolute expiry, in hours |
| `HTTP_READ_TIMEOUT` | int | 15 | no | yes | 1..300 | HTTP server read timeout, in seconds |
| `HTTP_WRITE_TIMEOUT` | int | 15 | no | yes | 1..300 | HTTP server write timeout, in seconds |
| `HTTP_IDLE_TIMEOUT` | int | 60 | no | yes | 1..300 | HTTP server idle timeout, in seconds |
| `PROFILE_PHOTO_DIR` | string | — | yes | yes | absolute path | Directory storing profile photos |
| `ATTACHMENTS_DIR` | string | (PROFILE_PHOTO_DIR's parent)/attachments | no | yes | absolute path | Directory storing contact attachments |
| `CARDDAV_ENABLED` | bool | false | no | yes | — | Enable the CardDAV contact-sync server |
| `CALDAV_ENABLED` | bool | false | no | yes | — | Enable the CalDAV interaction/life-event sync server |
| `CALDAV_TWO_WAY_ENABLED` | bool | false | no | yes | — | Allow calendar sync to push local edits back out |
| `COOKIE_SECURE` | bool | false | no | yes | — | Set Secure flag on the auth cookie (requires HTTPS) |
| `COOKIE_DOMAIN` | string | — | no | yes | hostname, optionally dot-prefixed | Domain for the auth cookie |
| `DISABLE_REGISTRATION` | bool | false | no | yes | — | Disable new user registration |
| `WEBHOOK_BLOCK_PRIVATE_URLS` | bool | false | no | yes | — | Block webhook deliveries to private/loopback addresses |
| `CALDAV_SYNC_INTERVAL_HOURS` | int | 6 | no | yes | >=1, invalid value refuses to boot | Interval for the scheduled calendar sync job, in hours |
| `CALDAV_BLOCK_PRIVATE_URLS` | bool | false | no | yes | — | Block calendar sync requests to private/loopback addresses |
| `DELETED_RETENTION_DAYS` | int | 30 | no | yes | >=0, 0 disables the purge | Days soft-deleted rows survive before the purge job hard-deletes them |
| `AUDIT_RETENTION_DAYS` | int | 90 | no | yes | >=0, 0 disables the purge | Days audit events survive before the retention purge removes them |
| `CONTACT_SHARE_RETENTION_DAYS` | int | 30 | no | yes | >=0, 0 disables the purge | Days a ContactShare snapshot survives before the purge job hard-deletes it |
| `SYSTEM_EVENT_RETENTION_DAYS` | int | 30 | no | yes | >=0, 0 disables the purge | Days system_events rows survive before the retention purge removes them |
| `WEBHOOK_DELIVERY_RETENTION_DAYS` | int | 30 | no | yes | >=0, 0 disables the purge | Days webhook_deliveries rows survive before the purge job hard-deletes them |
| `JOB_RUN_RETENTION_DAYS` | int | 30 | no | yes | >=0, 0 disables the purge | Days job_runs rows survive before the retention purge removes them |
| `IDEMPOTENCY_KEY_RETENTION_HOURS` | int | 24 | no | yes | any integer, <=0 disables | Hours idempotency_keys rows survive before the TTL purge removes them |
| `SESSION_IDLE_TIMEOUT_HOURS` | int | 12 | no | yes | 0 (disabled) or 1..JWT_EXPIRY_HOURS | Hours a session may sit unused before AuthMiddleware rejects it |
| `PER_USER_CONTACT_LIMIT` | int | 0 | no | yes | >=0, 0 disables the quota, invalid value refuses to boot | Maximum live contacts a single user may hold (0 = unlimited) |
| `PER_USER_NOTE_LIMIT` | int | 0 | no | yes | >=0, 0 disables the quota, invalid value refuses to boot | Maximum live notes a single user may hold (0 = unlimited) |
| `PER_USER_RELATIONSHIP_EDGE_LIMIT` | int | 0 | no | yes | >=0, 0 disables the quota, invalid value refuses to boot | Maximum live relationship edges a single user may hold (0 = unlimited) |
| `PER_USER_ATTACHMENT_QUOTA_MB` | int | 0 | no | yes | >=0, 0 disables the quota, invalid value refuses to boot | Maximum attachment bytes a single user may store, in MiB (0 = unlimited) |
| `API_RATE_LIMIT_INTERVAL_MS` | duration | 600ms | no | yes | >0ms | Sustained rate-limit refill interval (milliseconds) |
| `API_RATE_LIMIT_BURST` | int | 1000 | no | yes | >=1 | Rate-limit bucket size, largest instantaneous burst allowed |
| `AUTH_SPRAY_ENABLED` | bool | true | no | yes | — | Master switch for the instance-wide failed-auth velocity signal |
| `AUTH_SPRAY_WINDOW_SECONDS` | int | 60 | no | yes | >=1, invalid/low values clamped to 60 with a WARN | Sliding window velocity is measured over, in seconds |
| `AUTH_SPRAY_FAILURE_THRESHOLD` | int | 60 | no | yes | >=1, invalid/low values clamped to 60 with a WARN | Failures within the window that arm the signal |
| `AUTH_SPRAY_IDENTIFIER_THRESHOLD` | int | 15 | no | yes | >=1, invalid/low values clamped to 15 with a WARN | Distinct identifiers within the window that arm the signal |
| `AUTH_SPRAY_THROTTLE_SECONDS` | int | 300 | no | yes | >=1, invalid/low values clamped to 300 with a WARN | How long a tripped signal refuses unknown sources, in seconds |
| `IMMICH_SYNC_INTERVAL_HOURS` | int | 6 | no | yes | >=1, invalid value refuses to boot | Interval for the scheduled Immich enrichment sync, in hours |
| `IMMICH_BLOCK_PRIVATE_URLS` | bool | false | no | yes | — | Block Immich fetches to private/loopback addresses |
| `PAPERLESS_BLOCK_PRIVATE_URLS` | bool | false | no | yes | — | Block Paperless-ngx fetches to private/loopback addresses |
| `SEAFILE_BLOCK_PRIVATE_URLS` | bool | false | no | yes | — | Block Seafile fetches to private/loopback addresses |
| `WEBDAV_BLOCK_PRIVATE_URLS` | bool | false | no | yes | — | Block Nextcloud/ownCloud WebDAV fetches to private/loopback addresses |
| `MONICA_BLOCK_PRIVATE_URLS` | bool | false | no | yes | — | Block Monica import-assistant fetches to private/loopback addresses |
| `FCM_SERVICE_ACCOUNT_FILE` | string | — | no | yes | path to an existing file | Path to the Firebase service-account JSON for FCM mobile push |
| `DB_INTEGRITY_CHECK_ENABLED` | bool | true | no | yes | — | Enable the scheduled live-DB PRAGMA integrity_check job |
| `DB_INTEGRITY_CHECK_INTERVAL_HOURS` | int | 24 | no | yes | >=1, invalid value refuses to boot | Interval for the scheduled DB integrity check, in hours |
| `DB_RESTORE_DRILL_ENABLED` | bool | true | no | yes | — | Enable the scheduled backup-restore drill job |
| `DB_RESTORE_DRILL_INTERVAL_HOURS` | int | 168 | no | yes | >=1, invalid value refuses to boot | Interval for the scheduled restore drill, in hours |
| `DB_RESTORE_DRILL_MAX_DURATION_SECONDS` | int | 0 | no | yes | >=0, invalid value refuses to boot | RTO budget for the database piece of a restore; 0 means no budget |
| `ALERTING_ENABLED` | bool | true | no | yes | — | Master switch for the alert evaluator |
| `ALERT_EVAL_INTERVAL_MINUTES` | int | 15 | no | yes | >=1, invalid value refuses to boot | How often the alert evaluator runs, in minutes |
| `ALERT_DISK_USAGE_PERCENT` | int | 90 | no | yes | 0..99, invalid value refuses to boot | Raise disk_space when used% >= this; 0 disables the condition |
| `ALERT_SYNC_FAILURE_THRESHOLD` | int | 3 | no | yes | >=1, invalid value refuses to boot | Consecutive sync failures before sync:* fires |
| `ALERT_NOTIFY_FAILURE_THRESHOLD` | int | 3 | no | yes | >=1, invalid value refuses to boot | Consecutive notification failures before the notifications condition fires |
| `ALERT_BACKUP_MAX_AGE_HOURS` | int | 0 | no | yes | >=0, invalid value refuses to boot | Raise backup_stale when the last backup success is older than this; 0 means 2x DB_RESTORE_DRILL_INTERVAL_HOURS |
| `ALERT_JOB_STALE_MULTIPLIER` | int | 3 | no | yes | >=2, invalid value refuses to boot | Raise job_stopped when a job's last successful run is older than interval times this |
| `ALERT_INCIDENT_QUIET_HOURS` | int | 6 | no | yes | >=1, invalid value refuses to boot | integrations recovers when no new integration_failed event lands within this window, in hours |
| `ALERT_BACKUP_ENABLED` | bool | true | no | yes | — | Enable the backup / backup_stale conditions |
| `ALERT_DB_INTEGRITY_ENABLED` | bool | true | no | yes | — | Enable the db_integrity condition |
| `ALERT_JOB_STOPPED_ENABLED` | bool | true | no | yes | — | Enable the job_stopped condition |
| `ALERT_AUTH_SPRAY_ENABLED` | bool | true | no | yes | — | Enable the auth_spray condition |
| `HIBP_CHECK_ENABLED` | bool | false | no | yes | — | Check new/changed passwords against HIBP's k-anonymity range API |
| `UPDATE_CHECK_ENABLED` | bool | false | no | yes | — | Compare the running build against the latest GitHub release |
| `STORAGE_WARN_PERCENT` | int | 75 | no | yes | 1..99, invalid value refuses to boot | usage% >= this turns the storage threshold warning |
| `STORAGE_CRITICAL_PERCENT` | int | 90 | no | yes | >STORAGE_WARN_PERCENT and <=100, invalid value refuses to boot | usage% >= this turns the storage threshold critical |
| `STORAGE_SAMPLE_RETENTION_DAYS` | int | 180 | no | yes | >=7, invalid value refuses to boot | Days of storage_samples history kept |
| `*(derived: OIDC.Enabled)*` | — | — | no | no | — | true when OIDC_PROVIDER_URL, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET are all set |
| `OIDC_PROVIDER_URL` | string | — | no | yes | absolute http(s) URL | OIDC provider issuer URL |
| `OIDC_CLIENT_ID` | string | — | no | yes | — | OIDC client ID |
| `OIDC_CLIENT_SECRET` | string | — | no | yes | — | OIDC client secret |
| `*(derived: OIDC.RedirectURL)*` | — | — | no | no | — | derived from FRONTEND_URL, not configurable |
| `OIDC_AUTO_PROVISION` | bool | false | no | yes | — | Auto-create a local account on first OIDC login |
| `OIDC_TRUST_EMAIL` | bool | false | no | yes | — | Skip email_verified requirement when linking accounts |
| `OIDC_SCOPES` | stringlist | openid,email,profile | no | yes | — | Comma-separated OIDC scopes |
| `*(derived: OIDC.PostLogoutRedirectURL)*` | — | — | no | no | — | derived from FRONTEND_URL, not configurable |
| `OIDC_BLOCK_PRIVATE_URLS` | bool | false | no | yes | — | Route OIDC discovery/token/JWKS/UserInfo calls through the SSRF-guarded dialer |
| `DATA_ENCRYPTION_KEY` | string | — | no | yes | base64-encoded 32 random bytes | At-rest field-encryption master key |
| `DATA_ENCRYPTION_KEY_FILE` | string | — | no | yes | path to an existing file | Path to a file whose trimmed contents are the master key |
| `METRICS_TOKEN` | string | — | no | yes | >=16 characters when set | Bearer token gating GET /metrics; unset leaves the route unregistered |
| `MIN_CLIENT_VERSION` | string | — | no | yes | major[.minor[.patch]] optionally with -prerelease/+build | Oldest Android client versionName this server still supports |
| `DEMO_MODE` | bool | false | no | yes | — | Disable registration-adjacent writes (password change, photo upload) for a public demo deployment |
| `LOG_LEVEL` | enum | info | no | yes | debug\|info\|warn\|error\|fatal\|panic | Structured log verbosity |
| `LOG_PRETTY` | bool | false | no | yes | — | Console-formatted (vs. JSON) log output; always on when GIN_MODE != release, regardless of this setting |
| `GIN_MODE` | enum | debug | no | yes | debug\|release\|test | Gin framework mode; also read directly by the gin package itself |

## Variables outside config.Config

Read directly from the environment outside `config.LoadConfig` (issue #936) — see each row's "Why outside Config" for why it cannot simply move onto `Config`.

| Variable | Type | Default | Required | Restart required | Range / enum | Description | Why outside Config |
|---|---|---|---|---|---|---|---|
| `MYCORRHIZAL_ALLOW_SUB_FLOOR_MIGRATION` | bool | false | no | yes | true to enable; any other value (including unset) leaves it off | One-time bridge allowing a pre-v0.6.0 database to migrate (issue #529) | read by database.InitDB during migration bootstrap, before config.LoadConfig runs — see docs/upgrade-compatibility.md |
| `MYCORRHIZAL_PRE_MIGRATION_BACKUP_DIR` | string | a 'pre-migration' sibling directory of SQLITE_DB_PATH | no | yes | directory path | Where the automatic pre-migration backup snapshot is written (issue #530) | read by database.preMigrationBackupDir during migration bootstrap, before config.LoadConfig runs |
