Skip to main content
Vexa is configured by environment variables, set in deploy/compose/.env for Docker Compose (seeded from .env.example on first make all). Defaults below are what the stack falls back to when the variable is unset — fine for local evaluation, not for anything exposed.
Change every secret (ADMIN_TOKEN, INTERNAL_API_SECRET, DB and MinIO credentials, VEXA_DISPATCH_SIGNING_KEY, NEXTAUTH_SECRET) before putting the stack on a network. The defaults are public.

Transcription (STT)

The STT service is the GPU workload, deployed separately from this stack — see Deployment → Transcription. The main stack stays GPU-free and reaches it over the network via the two variables above.

When the backend is wrong, you learn at configure time

A URL or token that does not work is refused where you set it, not by an empty transcript hours later. The same live check runs at three surfaces, and all three name the same cause: The verdict is cached for 60s, so after fixing the value either wait a minute or call GET /health?force=1 to re-probe immediately.
A 404 from the transcriptions path is treated as a wrong URL, not as proof of life: a real OpenAI-compatible endpoint answers 400 or 401 to an empty request body and never 404. Some gateways also answer 404 for a rejected credential — the error text names both possibilities.
Never put a comment on the same line as a value in .env. Write it on its own line above the key. Compose tolerates a trailing # comment, but docker run --env-file (which Vexa Lite uses) does not strip it — the comment becomes part of the value, so TERMINAL_PORT=13000 # the UI sets the port to the literal string 13000 # the UI.

Optional service authority

Stock self-hosted Vexa has no external service authority and continues to admit bots exactly as before. Operators who need an independently owned admission/active-service policy can opt into the sealed service-authority.v1 boundary: The request contains service identity, authoritative user ID, service mode, frozen transcription provider, concurrency, and lifecycle timestamps. It deliberately contains no email, customer or payment-provider identifier, price, balance, transcription endpoint URL, or credential. A configured authority that is unavailable, malformed, stale, or bound to another request fails closed before a new bot is spawned. During active service, its stop intent is committed before runtime teardown and resumed after restart. mode: "observe" records decisions without refusing or stopping service. It is useful for rollout, but it is not evidence that a hard service control is enforced. A session admitted in one mode is never reinterpreted under another mode after a deployment change.

Operator terminal callback

An operator can send terminal service facts to one deployment-owned system endpoint. This is separate from a user’s webhook configuration: the URL is frozen at boot, cannot be supplied by an API caller or meeting payload, and receives only meeting.completed and bot.failed. Transient delivery failures enter a dedicated retry/dead-letter queue and retain the same deterministic event ID. Customer webhook destinations continue through DNS/IP validation and cannot redirect this operator callback. Leave all four values unset for the stock OSS behavior.

Secrets & identity

Bot participant name

The terminal client uses NEXT_PUBLIC_DEFAULT_BOT_NAME at build time and falls back to Vexa. The meeting-api uses DEFAULT_BOT_NAME for raw POST /bots callers and falls back to VexaBot-{random}. Direct joinMeeting callers use the same variable and fall back to Vexa Join Layer. An explicit bot_name in the request takes precedence over these defaults.

Database & storage

DB_POOL_SIZE / DB_MAX_OVERFLOW are pure runtime overrides. On managed Postgres with a hard max_connections, reconcile them with the per-service connection budget in deploy/db-budget.json, whose accounting is Σ (replicas × (pool_size + max_overflow)) + reserved ≤ max_connections. The defaults (5 / 10) match that budget.
| MINIO_ENDPOINT | minio:9000 | Object storage endpoint (recordings + workspaces). | | MINIO_ACCESS_KEY / MINIO_SECRET_KEY | vexa-access-key / vexa-secret-key | MinIO credentials. | | MINIO_ROOT_USER / MINIO_ROOT_PASSWORD | vexa-access-key / vexa-secret-key | MinIO root credentials. | | MINIO_BUCKET | vexa | Bucket holding recordings and agent workspaces. | | MINIO_SECURE | false | Use TLS to reach MinIO. |

Agent inference (bring your own)

Point the agent at your own model so no inference leaves the network.

Claude subscription credentials (HOST_CLAUDE_CREDENTIALS)

Setting HOST_CLAUDE_CREDENTIALS=~/.claude/.credentials.json mounts your Claude Code sign-in into the agent containers (read-only), so the agent runs on your subscription instead of an API key. Whether that file stays valid depends on the host OS:
  • Linux (and Windows via WSL2 — run the stack and the claude CLI inside WSL): the file is Claude Code’s own store; the CLI refreshes it in place. Nothing to do.
  • macOS: the CLI’s source of truth is the login Keychain — the file is a one-time export whose token expires every ~8–12 hours. Symptom: agent chat fails with 401 Invalid authentication credentials while claude works fine in your terminal. Install the bundled sync daemon once:
    It registers a launchd user agent (ai.vexa.claude-creds-sync) that re-exports the Keychain into the file every 5 minutes, write-only-on-change, preserving the inode the containers mount. install.sh uninstall removes it. Details: deploy/bin/claude-creds-sync/README.md.
The credential-file mount is a single-developer convenience. For a portable setup that survives token rotation on any OS, configure an API key or a custom endpoint in Settings → Models (stored per-user/global in the database) — see the setup wizard or the table above.

Auto-join & calendar sync

Timing knobs for the two meeting-api background sweeps (auto-join and calendar sync). Both degrade gracefully: without ADMIN_API_URL + INTERNAL_API_SECRET, calendar sync no-ops and auto-join spawns without per-user context — the stack still boots.

Images & runtime

Ports

Host ports the compose stack publishes on 127.0.0.1 (override any of them in .env):
The gateway (:18056) is the one front door; the terminal web workbench is at :13000. The other host ports above are bound to 127.0.0.1 for local inspection and aren’t needed for day-to-day use.

Gateway edge protection

The gateway carries two independent abuse layers. Both are env-driven; both default sensibly for self-hosted and can be left untouched.

Per-user limiter (post-auth, on by default)

A token-bucket limiter keyed by user id fires after the API key is resolved — it catches one token driving too much traffic. On for self-hosted by default.

Edge guard (pre-auth, off by default for self-hosted)

An optional fastapi-guard edge layer caps requests per client IP before the API key is validated — so an IP flooding invalid keys, or rotating many keys from one IP to defeat the per-user limiter, is answered with 429 at the edge and never reaches admin-api. An IP that keeps offending past a threshold is auto-banned for a window; sustained abuse costs the abuser, not the operator. Hosted runs this on; self-hosted defaults OFF. The code default is GUARD_ENABLED=true; the deploy surfaces set GUARD_ENABLED=false for self-hosted, so you opt in by overriding it.
GUARD_ENABLED=false is the kill switch — flip it to true to turn the whole edge layer on. When behind a reverse proxy you must also set GUARD_TRUSTED_PROXIES, or every request keys to the proxy IP → one global bucket shared by all clients (one abuser throttles everyone). See Deployment → Publishing behind a reverse proxy.
With GUARD_ENABLE_REDIS=false AND uvicorn --workers N>1, the HTTP rate-limit buckets and auto-bans are also per-process: the effective limit becomes N × GUARD_RATE_LIMIT_RPM and bans do not propagate across workers (the WS per-process ceiling above applies to the WS path independently). The same applies across multiple gateway replicas without Redis — each is its own process.Safe configurations: the shipped gateway runs a single uvicorn worker, so the default (GUARD_ENABLE_REDIS=true, one worker) enforces the limit globally. If you scale to multiple workers or replicas, keep GUARD_ENABLE_REDIS=true — Redis-backed state is what shares the buckets and bans across processes. GUARD_ENABLE_REDIS=false is only safe with a single worker and a single replica; if you must run without Redis at higher concurrency, divide GUARD_RATE_LIMIT_RPM by the process count to hold the aggregate cap (note this still will not propagate bans).
The guard fails open (fail_secure=false): a guard-check bug or a redis outage returns the request to the app rather than taking the gateway down. Request-body WAF scanning is intentionally off — the gateway proxies arbitrary user text (chat, meeting data, transcript shares), so signature scanning would false-positive on legitimate content.