You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

5.8 KiB

Configuration

Every env var this app reads is parsed and validated in one place: src/lib/server/core/config.ts's loadConfig(). It reads process.env directly (not $env/static/private) for the same reason tokens.ts and dev-plugin.ts already do — this module tree is compiled standalone (tsc -p tsconfig.server.json) for server.js to import outside Vite's module graph. See .env.example for the full list with inline comments.

AppConfig shape

interface AppConfig {
	port: number;
	openaiApiKey: string;
	analytics: AnalyticsConfig; // { enabled, dbPath, messages, toolCalls, events }
	session: SessionRuntimeConfig; // { modelOverride?, defaultVoice?, clientToolTimeoutMs, tokenTtlMs }
}

AnalyticsConfig extends AnalyticsCategoryConfig (defined in analytics.ts itself, not here — it's the recorder's own config shape, the same way SessionDefaults lives in session.ts).

Env vars

Var Default Required
OPENAI_API_KEY — yes
REALTIME_TOKEN_SECRET — yes
PORT 3000 no
ANALYTICS_ENABLED true no
ANALYTICS_DB_PATH ./data/analytics.db no
ANALYTICS_LOG_MESSAGES true no
ANALYTICS_LOG_TOOL_CALLS true no
ANALYTICS_LOG_EVENTS true no
SESSION_MODEL SESSION_PERSONA.model (see below) no
SESSION_DEFAULT_VOICE unset — provider picks no
CLIENT_TOOL_TIMEOUT_MS 10000 no
REALTIME_TOKEN_TTL_MS 60000 no

Booleans accept exactly "true"/"false"; anything else (if the var is set at all) is a validation error. Integers are validated for range where one applies (e.g. PORT must be 1–65535).

Fail-fast, in both dev and prod

loadConfig() collects every problem — missing required var, malformed boolean/integer, out-of-range integer — into a single thrown Error listing all of them, rather than failing on the first one found. It's called once, eagerly, at both composition roots (server.js's top level; dev-plugin.ts's configureServer, right after .env is loaded), and memoized after that (env vars don't change mid-process, and this gets called again from the session-token route to read tokenTtlMs).

This means npm run dev now requires a fully-populated .env to start at all — previously, a missing OPENAI_API_KEY only produced a console warning in dev and the failure surfaced later, confusingly, inside a WS handshake attempt. Prod already threw eagerly; this just makes dev match it, consistent with this repo's existing "fail loud at boot" precedent (see createToolRegistry's doc comment in server/core/tools.ts).

REALTIME_TOKEN_SECRET is a deliberate partial exception: loadConfig() checks it's present (so a missing secret is caught at the same startup point as everything else) but does not store its value on AppConfig — tokens.ts remains the sole reader of the actual secret, reading process.env directly and lazily, exactly as before. This is what keeps tokens.spec.ts's existing pattern of mutating process.env.REALTIME_TOKEN_SECRET per test working unchanged.

The vite.config.ts / Vitest gotcha

Do not move a throwing, env-validating call to vite.config.ts's top level, and don't have it build an already-resolved session-defaults object either. Both Vitest projects in vite.config.ts's test.projects use extends: './vite.config.ts', so this file's top-level code — and any plugin configureServer hook — runs on every npm run test:unit, not just npm run dev. dev-plugin.ts's configureServer already has to guard itself with if (process.env.VITEST) return for exactly this reason.

Concretely: vite.config.ts passes only raw, env-independent pieces into the dev plugin — realtimeDevWsPlugin({ persona: SESSION_PERSONA, toolRegistry }) — never a pre-resolved defaults/config object. The loadConfig() call, resolveSessionDefaults(...), and the NoopAnalyticsRecorder/SqliteAnalyticsRecorder choice all happen inside configureServer, below the VITEST guard. If a future change needs another env-driven value in dev, thread it through the same way — resolve it inside configureServer, not at vite.config.ts module scope — or npm run test:unit will start requiring a populated .env for no reason, and may pick up a developer's real local secrets into process.env during test runs.

Session persona vs. runtime overrides

The actual persona — model choice and system instructions — is project-owned content in src/lib/project/persona.ts, not part of this config system (see docs/TOOLS.md for why core/project content is split this way). config.ts's session.modelOverride (from SESSION_MODEL) lets a deployment swap the model without touching that file; instructions deliberately has no env override — it's prose, and belongs in a commented TypeScript file, not a .env value.

What's not configurable (on purpose)

src/lib/server/core/providers/openai.ts's hardcoded upstream URL, its always-['audio'] output_modalities, and its tool_choice: 'auto' are deliberately out of scope here — these are provider-tuning knobs nobody asked to change yet, and adding config surface for them now would be speculative.