Configuration
Every environment variable, config key, and feature flag, organised by service.
Configuration
Every service in the Wordloop platform loads its configuration from environment variables, following the Twelve-Factor App config principle. This page is the canonical catalogue of those variables — what they do, what their defaults are, and which service owns them.
Local defaults are generated by ./dev setup env. The variables listed here are the full contract; your local .env files typically override only the subset you need.
Common variables
Variables consumed by multiple services.
| Variable | Service(s) | Default (local) | Purpose |
|---|---|---|---|
APP_ENV | all | development | development, test, staging, production. Controls auth mode, logging verbosity, and feature defaults. |
DATABASE_URL | core | derived | Postgres connection string. |
PUBSUB_EMULATOR_HOST | core, ml | localhost:8085 | Local Pub/Sub emulator. Unset in production. |
OTEL_EXPORTER_OTLP_ENDPOINT | all | http://localhost:4318 | Collector endpoint for traces, metrics, and logs. |
LOG_LEVEL | all | info | debug, info, warn, error. |
wordloop-core
| Variable | Default | Purpose |
|---|---|---|
CORE_PORT | 4002 | HTTP + WebSocket port. |
CLERK_SECRET_KEY | — | Backend Clerk key for JWT verification. |
CLERK_PUBLISHABLE_KEY | — | Frontend-shared key; surfaced for debug. |
STORAGE_BUCKET | wordloop-local-audio | GCS bucket for audio artefacts. |
AUDIO_URL_SIGNING_KEY | — | HMAC key for audio playback proxy links (GET /meetings/{id}/audio). Signs {meeting_id}:{user_id}:{exp}, keeping playback links a per-user capability. Required outside APP_ENV=test — Core refuses to start without it. Dedicated to this purpose: reusing SERVICE_AUTH_TOKEN would let every holder of the service token mint a playback link for any user's audio. Generate with openssl rand -hex 32. In APP_ENV=test only, an unset key is derived from SERVICE_AUTH_TOKEN via HMAC (never the token itself), with a startup warning. |
RECORDING_* — live audio durability and recovery
Tunes the live-recording lifecycle: segment storage, progress events, gap recovery, and shutdown timing. Source: services/wordloop-core/internal/config/config.go (RecordingConfig).
| Variable | Default | Purpose |
|---|---|---|
RECORDING_SEGMENT_TARGET_MS | 2000 | Target duration of one GCS storage segment. |
RECORDING_PROGRESS_EVERY_FRAMES | 100 | Emits AudioStoredProgressEvent every N frames. |
RECORDING_PROGRESS_INTERVAL_SECONDS | 10 | Emits AudioStoredProgressEvent at least this often. |
RECORDING_GAP_UPLOAD_DEADLINE_SECONDS | 1800 | How long after stop gap chunks are accepted. |
RECORDING_GAP_SWEEP_INTERVAL_SECONDS | 30 | Deadline sweeper cadence — how often Core checks for recordings past their gap-upload deadline. |
RECORDING_STOP_SETTLE_MS | 500 | How long Core keeps accepting trailing audio frames after a stop command. |
RECORDING_ML_DRAIN_TIMEOUT_SECONDS | 5 | Bounds the ML drain call during stop. |
RECORDING_DISCONNECT_GRACE_SECONDS | 120 | How long Core waits after a user's last WebSocket connection closes before stopping their live recording with reason connection_closed. A reconnect or resume inside the window cancels the stop. 0 stops the recording as soon as the connection drops. The default covers four attempts at the App's 30s maximum reconnect backoff. |
RECORDING_MAX_CHUNK_BYTES | 1048576 (1 MiB) | Largest gap-recovery chunk accepted. |
RECORDING_MAX_DURATION_SECONDS | 14400 (4h) | Absolute ceiling for one recording. A StartRecordingCommand may lower it per recording, never raise it. |
RECORDING_AUDIO_URL_TTL_SECONDS | 900 (15 min) | How long a playback link from GET /meetings/{id}/audio-url stays valid. |
wordloop-ml
| Variable | Default | Purpose |
|---|---|---|
ML_PORT | 4003 | FastAPI port. |
MODEL_PROVIDER | anthropic | Chooses which model adapter to load. |
ANTHROPIC_API_KEY | — | Set when MODEL_PROVIDER=anthropic. |
OPENAI_API_KEY | — | Set when MODEL_PROVIDER=openai. |
ML_CACHE_TTL_SECONDS | 3600 | Cache lifetime for deterministic model calls. |
ML_* — live insight cadence, streaming durability, and no-audio detection
Source: services/wordloop-ml/src/wordloop/config/settings.py (Settings).
| Variable | Default | Purpose |
|---|---|---|
ML_TALKING_POINT_CADENCE_SEGMENTS | 4 | Unprocessed live transcript segments that trigger a draft talking-point extraction. |
ML_TALKING_POINT_CADENCE_SECONDS | 30.0 | Seconds since the last extraction after which the next segment triggers one regardless of segment count (whichever cadence fires first). |
ML_TASK_CADENCE_SEGMENTS | 12 | Unprocessed live transcript segments that trigger a draft task extraction. |
ML_SEGMENT_FEATURE_MAX_PER_SPEAKER | 8 | Upper bound of batch transcript segments per speaker label that get a voice feature_vector (the longest ones are chosen). 0 disables feature emission. |
ML_SEGMENT_FEATURE_DIMENSIONS | 192 | Dimension Core's transcript_segments.feature_vector / people.voice_vector columns (pgvector vector(192)) accept; matches the ECAPA-TDNN ONNX output. An embedding model change must be paired with a Core migration — see ADR 0008. |
ML_BACKPRESSURE_QUEUE_DEPTH | 64 | Per-session audio send-queue depth above which backpressure is reported. |
ML_AUDIO_QUEUE_CAPACITY | 512 | Hard bound of the per-session send queue; frames beyond it are dropped and counted. |
ML_INSIGHT_WARNING_GAP_FRAMES | 300 | A single sequence gap of this many frames (30s at 100ms) reports insight_warning. |
ML_DRAIN_TIMEOUT_SECONDS | 0.5 | Upper bound DELETE /streaming/{id} waits for the queue to drain before answering. |
ML_INSIGHT_CONTEXT_SEGMENTS | 60 | Recent transcript segments pulled from Core to rebuild insight context on resume. |
ML_NO_AUDIO_WINDOW_SECONDS | 10.0 | Audio duration over which every decodable frame must be silent before no_audio_detected is reported (the test stack uses 1). |
ML_NO_AUDIO_RMS_THRESHOLD | 50.0 | Per-frame RMS (int16 scale, 0-32767) below which a PCM16 frame counts as silent. |
ML_NO_AUDIO_BATCH_FRAMES | 10 | Containerised (webm/ogg) frames decoded together for no-audio scoring. At the browser's 100ms timeslice this is ~1s of audio per codec call, so ffmpeg runs about once a second per session instead of once per frame. Raw PCM16 streams ignore this — they are scored inline without a codec. |
wordloop-app
| Variable | Default | Purpose |
|---|---|---|
NEXT_PUBLIC_CORE_URL | http://localhost:4002 | URL the browser uses to reach Core. |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | — | Clerk frontend key. |
APP_PORT | 4001 | Next.js port. |
Feature flags
Feature flags are served dynamically — they are not environment variables. See the flag dashboard for the current state and owners. Progressive-delivery principles (Progressive Delivery) govern how flags are created, rolled, and retired.
Further reading
- Quickstart — bootstrapping local
.envfiles. - Security — the rules around secret handling.
- Twelve-Factor App — the philosophy behind environment-based config.