Docs / reference

Configuration reference

Every environment variable read by the application, with its exact default. Task-oriented guidance is in Configuration.

Variable names are case-insensitive. The server reads ../.env relative to backend/; Compose passes ./.env into the containers.

Required

VariableDefaultNotes
JWT_SECRET_KEY—Required. ≥ 32 characters of real randomness. Startup refuses placeholders and short values
DATABASE_URL—Required. e.g. postgresql+asyncpg://user:pass@host:5432/db. Postgres must be a pgvector image

Environment and mode

VariableDefaultNotes
ENVIRONMENTdevelopmentdevelopment | production | test. Production enables secure cookies and HSTS, and disables /docs and /redoc
LLM_MODErealreal | fake. Fake uses scripted models and fixture retrievers — deterministic, keyless, no network
FRONTEND_URLhttp://localhost:3031Dev only. The CORS allow-list. Production uses the same-origin proxy and enables no CORS at all

Provider keys

VariableDefaultNotes
GOOGLE_API_KEY""
ANTHROPIC_API_KEY""
OPENAI_API_KEY""
OPENROUTER_API_KEY""
CUSTOM_API_KEY""Any OpenAI-compatible endpoint
CUSTOM_BASE_URL""Required alongside CUSTOM_API_KEY

With ENVIRONMENT=production and LLM_MODE=real, startup fails if a routed provider has no key. ollama and custom are exempt — they need none.

Model routing

"provider:model", split on the first colon only.

VariableDefault
MODEL_PLANNERgoogle:gemini-2.5-pro
MODEL_EXECUTORgoogle:gemini-2.5-flash
MODEL_CRITICgoogle:gemini-2.5-flash
MODEL_SYNTHESIZERgoogle:gemini-2.5-pro
MODEL_CHATgoogle:gemini-2.5-flash

Startup refuses a routed model with no price entry in the catalog. openrouter, custom, and ollama are exempt from that check — with the consequence that the per-session cost cap cannot fire on the first two.

Local models

VariableDefaultNotes
OLLAMA_BASE_URLhttp://localhost:11434/v1Use http://host.docker.internal:11434/v1 when the app runs in Docker

Search retrievers

VariableDefaultNotes
TAVILY_API_KEY""Optional. First in the chain
BRAVE_API_KEY""Optional. Second

With neither set, DuckDuckGo is the only retriever — keyless, but rate-limited and slow.

Data stores

VariableDefaultNotes
REDIS_URLredis://localhost:6379/0Broker, SSE fan-out, rate limits, locks, search cache
CORPUS_DIRdata/corpusWhere per-project corpus SQLite files live. A relative value resolves against the backend package root, not the process working directory

Authentication

VariableDefaultNotes
JWT_ALGORITHMHS256
JWT_ACCESS_TOKEN_EXPIRE_MINUTES15
JWT_REFRESH_TOKEN_EXPIRE_DAYS14
REQUIRE_EMAIL_VERIFICATIONfalseSet true for any public deployment
ENCRYPTION_KEY""Encrypts users' stored provider keys. Empty derives from JWT_SECRET_KEY via HKDF under a distinct label. Set explicitly in production, so rotating the JWT secret does not invalidate every stored key

Run limits

All of these are 0 = unlimited, and 0 is the default unless stated otherwise.

VariableDefaultNotes
MAX_CRITIC_LOOPS2Retries per task before it settles with what it has
MAX_COST_PER_SESSION_USD0.0Inert on openrouter and custom
MAX_WALLCLOCK_SECONDS0
MAX_INPUT_TOKENS0Cumulative across critic loops and rework. The guard that works on every provider
MAX_PARALLEL_TASKS41 is the only value where a run cannot overshoot the cost cap
CELERY_TASK_TIMEOUT_SECONDS660Not a 0 = unlimited setting

Per-user limits

VariableDefaultNotes
DEFAULT_MONTHLY_TOKEN_LIMIT0Applied to new accounts only. 0 = unlimited
RESEARCH_RATE_LIMIT_PER_HOUR00 = unlimited. Enforced before model routing is consulted
CHAT_RATE_LIMIT_PER_HOUR00 = unlimited

Login and registration limits are not configurable: 20/min per IP and 5 failures per 15 min per account for login, 5/hour per IP for registration. They are brute-force protection, not usage caps, and an operator must not be able to disable them while raising a usage limit.

Embeddings

VariableDefaultNotes
EMBEDDINGS_PROVIDERautoauto | ollama | google | openai | none. auto prefers a reachable local Ollama, then a hosted provider with a key. none disables memory rather than degrading it
EMBEDDINGS_MODEL""Blank uses the provider's documented default. Changing this makes existing chunks invisible until re-indexed

Tracing

VariableDefault
LANGCHAIN_TRACING_V2false
LANGCHAIN_API_KEY""
LANGCHAIN_PROJECTmulti-agent-research-assistant

Compose-only variables

Read by docker-compose.full.yml, not by the application:

VariableDefault
POSTGRES_USERresearch_user
POSTGRES_PASSWORDresearch_pass
POSTGRES_DBresearch_db
FRONTEND_PORT3031
BACKEND_ORIGINhttp://api:8000 — baked into the frontend build as the /api rewrite target

Two configuration paths

The same run configuration is built twice, from two independent code paths:

PathBuilt byUsed by
Serverapp/runtime.py ← app/config.py (pydantic-settings)API, Celery worker
Localresearch_engine/local.py::run_config_from_env ← os.environCLI, evaluation harness, desktop sidecar

Adding a setting means adding it to both. This has drifted twice: once leaving OpenRouter unreachable from the local path, and once applying the production SSRF guard to a laptop and rejecting every local model server.

The engine's module defaults deliberately mirror the server settings' defaults exactly, so a host that forgets to install a configuration degrades to the documented behaviour rather than to something subtly different. A test pins that equivalence.

Settings not exposed as environment variables

Some run-configuration fields are set per user or per run rather than by the environment:

FieldSet byDefault
retrieval_kUser preferences5
min_sources_per_taskUser preferences0 (no floor)
snippet_max_charsUser preferences500
topic_seeds, outline_templateThe start requestempty / unset
skip_plan_gateThe start request; the app's form sends falsetrue at the API and engine level, false as the database column default
corpus_mode, demoThe start requestfalse
corpus_allowed_idsThe start request's corpus_selection, resolved once to exact document versions and frozen on the rununset (no selection)
offlineThe start request; implies corpus_modefalse
enforce_ssrf_guardsThe host — strict on the server, relaxed on desktoptrue
max_planner_tasksRun configuration6
prompt_overridesUser preferences, snapshotted onto the run at start (live for chat)empty