This page is about where secrets live and how they get there. For the full
annotated variable list per service — every key, its default, and what it does — see
the developer configuration page.
The model in one picture
Two rules follow from this diagram and they are the whole policy:- Source control holds code and
.env.exampleonly. The example file documents every variable a service expects, with placeholder values. The real.envis never committed and is listed in.gitignore. - Secrets enter at deploy time, not commit time. Jenkins pulls the real values
from its credential store (or from Bitbucket repository / deployment variables) and
writes them into the host’s
.envas the deploy step runs.
Each host gets a /opt/<service>/.env
Every Python service is started by systemd, and each unit points at a single
EnvironmentFile on that host. The service reads its configuration from that file and
nothing else — there are no config values hardcoded in the image, and no shared cluster
config service.
| Service | Host role | Env file | Loaded by |
|---|---|---|---|
| Backend API | API/App host | /opt/voxbridge/.env | voxbridge systemd service (uvicorn on 8080) |
| Voice fleet | Fleet host(s) | /opt/voxcore/.env | voxcore@.service (one instance per worker socket) |
| Dialler | SIP/LiveKit host | /opt/voxdialler/.env | voxdialler.service |
| Console | API/App host | build-time .env only | the Vite build — not at runtime (see below) |
EnvironmentFile=, so the values are loaded
into the process environment at start. The fleet’s templated unit is the only subtle
one — a single .env on the host is shared by every worker instance, and the
per-worker socket comes from the instance specifier, not from a secret:
How Jenkins injects secrets at deploy time
The deploy stage of the pipeline is the only place real secrets touch a host. On a green build ofmain, Jenkins connects to each target host, refreshes the code, syncs
dependencies, renders the host .env from credentials, then restarts the service.
See the pipeline and
deployment pages for the full flow; here is just the
secret-handling part.
Secrets live in the credential store
Provider keys, the JWT secret, database URIs and LiveKit keys are stored as
Jenkins Credentials (secret text / secret file), or as Bitbucket repository /
deployment variables for repo-scoped values. They are never in the repo and never
printed in build logs.
The deploy step binds them as variables
The pipeline binds the credentials it needs into the deploy step’s environment, so
they exist only for the duration of that step on the build agent.
Jenkins writes the host .env
The deploy step writes (or links)
/opt/<service>/.env on the target host from
those bound values — typically by rendering a template, or by copying a per-host
secret file. The file is owned by the service user and mode 600.Keep
.env.example in the repo accurate. It is the contract for what Jenkins must
supply — if a new variable is added to a service, add it to .env.example in the same
change so the deploy template and the credential store stay in step. A missing required
variable will stop the Backend from booting.Console secrets are build-time, not runtime
The Console is a static single-page app. It has no runtime.env — its
configuration is baked into the JavaScript bundle by Vite at build time through
VITE_* variables. This is a different and important rule:
- The build reads brand variables (
VITE_API_URL,VITE_BRAND_NAME, theme colours, the token storage key, etc.) and substitutes them intodist/duringnpm run build. - The output is served as static files by nginx. There is nothing to inject after the build; whatever was set at build time is permanent in that bundle.
- For Ori’s single-brand repo the brand
.envis committed at the brand path and the build copies it to the root.env, so the build is simplynpm install && npm run build.
The secrets that matter, per service
These are the sensitive values each service needs. Non-secret config (ports, log levels, intervals, sample rates) is covered on the developer configuration page.- Backend API
- Voice fleet
- Dialler
Set in
/opt/voxbridge/.env.JWT_SECRET— signs and verifies operator session tokens. The single most sensitive value in the platform.MONGODB_URI— connection string (may embed user/password) for the durable database.MONGODB_DBisvoxbridge.REDIS_URL— connection string for queues and runtime cache.
The shared VOXCORE_SECRET
VOXCORE_SECRET is the one secret that must be identical on two services: the
dialler presents it and the fleet verifies it. When the dialler attaches an answered
call to a free fleet worker over POST /attach, the fleet rejects the request unless
the secret matches. If you rotate it, rotate it on the fleet hosts and the dialler
host in the same window, or attaches will start failing.
Secret → service → where it’s set
| Secret | Used by | Where it’s set |
|---|---|---|
JWT_SECRET | Backend API | /opt/voxbridge/.env |
MONGODB_URI | Backend API, Dialler | /opt/voxbridge/.env, /opt/voxdialler/.env |
REDIS_URL | Backend API | /opt/voxbridge/.env |
SONIOX_API_KEY / DEEPGRAM_API_KEY (STT) | Voice fleet | /opt/voxcore/.env |
OPENAI_API_KEY / GOOGLE_API_KEY (LLM) | Voice fleet | /opt/voxcore/.env |
ELEVENLABS_API_KEY / SARVAM_API_KEY (TTS) | Voice fleet | /opt/voxcore/.env |
MINIO_ACCESS_KEY / MINIO_SECRET_KEY | Voice fleet | /opt/voxcore/.env |
OTEL_API_KEY | Voice fleet (when tracing on) | /opt/voxcore/.env |
LIVEKIT_API_KEY / LIVEKIT_API_SECRET | Dialler (LiveKit) | /opt/voxdialler/.env |
VOXCORE_SECRET | Dialler + Voice fleet (shared) | /opt/voxdialler/.env and /opt/voxcore/.env |
VITE_* (brand/config, not secret) | Console | build-time .env, baked into dist/ |
Rotating secrets safely
Secrets are not set once and forgotten. When you rotate one, do it deliberately and restart the services that read it. The general rotation procedure:Update the credential store
Change the value in Jenkins Credentials (or the Bitbucket variable). The repo and
.env.example do not change — only the stored secret.Redeploy the affected service(s)
Re-run the deploy job so Jenkins rewrites
/opt/<service>/.env on the target hosts
with the new value. For a shared secret like VOXCORE_SECRET, redeploy both the
fleet and the dialler.Restart and verify
The deploy restarts the unit (
systemctl restart …). Confirm the service came back
with the post-deploy health checks: Backend GET /health, fleet GET /health/fleet,
dialler GET /health on :8090. See the runbook.Where to go next
Full variable reference
Every environment variable for each service, with defaults and meaning.
The deploy pipeline
Bitbucket → Jenkins: build, test, and the deploy stage that writes the host
.env.Host deployment
Server roles, systemd units, and how each service is laid out on its host.
Operations runbook
Restart commands, health checks, and rollback after a rotation goes wrong.