Configuration
Vidra's configuration has two layers with a rule between them, and the rule is what makes the split predictable:
A value that cannot be changed safely while the process is running is environment-only. Everything else lives in the runtime settings registry and is editable from the admin console.
Secrets are always environment-only.
Layer 1: the environment file
config.Load reads the environment into one strongly validated structure and
validate() fails fast — in production it hard-refuses dev secrets, dev mail
capture, a plain-HTTP origin without explicit consent, and a range of other
unsafe combinations. The api does not boot on a bad config; it does not start
degraded.
env/production.env is generated by vidra setup from
env/production.env.example. That template is the authoritative, commented list
of every environment key, and it is where you should read them:
env/production.env.example— every key, with the comment explaining itenv/— per-environment templates (local, dev, QA, staging, production)
This page deliberately does not reproduce that list. A partial copy of a generated template is a copy that drifts, and a config key documented wrongly is worse than one not documented at all.
The keys most likely to bite are covered where they matter:
| Area | Page |
|---|---|
| Storage, S3 and IPFS | Storage backends |
| TLS topology and the edge | Production deployment |
| Search wiring and the shared HMAC | Search and discovery |
| Federation switches and key-encryption keys | Federation |
| Transcoding, packaging and codecs | The video pipeline |
| Compose profiles | Docker services |
Validate a file without deploying it:
vidra setup --check env/production.env
./deploy/compose.sh config -q # render check — catches missing required vars
make prod-config # the same, through the same wrapper
Never source env/production.env. VIDRA_COMPOSE_PROFILES=core frontend is
correct for Compose's --env-file parser and catastrophic for a shell, which
sets the variable to core and then tries to execute frontend. Parse it as
KEY=VALUE with the value literal, or hand it to docker compose --env-file.
Layer 2: the instance settings registry
internal/instancesettings is a typed, validated, runtime-mutable database
overlay. Each setting carries page and section metadata, and the admin
configuration UI is rendered from that metadata rather than hand-written.
Over a hundred settings are registered today.
A change takes effect without a restart: runtime knobs reach workers through provider-func closures read at request or job time, and the server reloads its overlay when a value is written.
The authoritative list is the registry itself:
vidra-core/internal/instancesettings/service.go— thespecsslice, in registry order
Read the effective values from a running instance instead of from a document:
curl -s https://example.com/api/v1/admin/system \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '.'
/api/v1/admin/system returns the effective non-secret configuration
alongside dependency health, which is the honest answer to "what is this instance
actually running with".
Effective capability is the setting AND the boot config
This is the one rule that explains most "the toggle did not do anything"
reports. A setting can only enable something the deployment actually has.
Turning captions on in the admin console does nothing if the captions profile
never started the Whisper service; enabling the IPFS mirror does nothing without
the ipfs profile and a reachable node.
Check both halves: the compose profiles in VIDRA_COMPOSE_PROFILES, and the
setting in the console.
Secret rotation costs
Not every secret is equally cheap to change. There is no re-wrap job — nothing re-encrypts persisted rows under a new key-encryption key.
| Secret | Cost |
|---|---|
JWT_SECRET | Free — logs everyone out. Also re-derives the playback-token signer, so outstanding ?pt= links stop working. Rotate off-peak. |
SMTP_PASSWORD | Free. Change at the relay and in the env file, then up -d api. |
STORAGE_S3_ACCESS_KEY / _SECRET_KEY | Free. Create the new key, deploy, then delete the old one — not the other way round. |
LIVE_INGEST_SECRET | Free. Breaks in-flight RTMP sessions; new stream keys work immediately. |
POSTGRES_PASSWORD / REDIS_PASSWORD | Free, but two places: the value and any explicit DSN that embeds it. Postgres needs ALTER ROLE … PASSWORD if the volume already exists. |
SEARCH_INTERNAL_SECRET | Must change on both services in one deploy. A split rollout means every core-to-search call 401s until it converges. |
FEDERATION_KEY_KEK | Destructive. Persisted federation actor keys become unreadable — and via the fallback below, stored ATProto app passwords and TOTP secrets too. |
MFA_KEY_KEK | Destructive. Every stored TOTP secret becomes undecryptable and every 2FA user is locked out. Leaving it unset stores TOTP secrets in plaintext, warned about only at boot. |
ATPROTO_KEY_KEK | Destructive. Every linked Bluesky app password becomes undecryptable; affected users must re-link. |
ATProtoKEK() and MFAKEK() both fall back to FederationKeyKEK when their own
value is unset. Rotating FEDERATION_KEY_KEK on an instance that never set the
other two silently destroys those too. Set all three explicitly.
If you must rotate an envelope key, the only safe procedure today is: set the new key, accept that the old ciphertext is dead, and force the affected users through re-enrolment. Plan it as a user-visible event, not an ops task.