Skip to content

Configuration

Every setting is read from an environment variable; the .env file is gitignored and never committed. In production the backend refuses to boot with placeholder or weak secrets, with a datastore URL still carrying an example or default password, with rate limiting switched off, or with TRUST_PROXY_HEADERS left unset. Every problem is reported in one message, naming the variables and never their values.

Generate the two required secrets before the first run:

openssl rand -hex 32       # JWT_SECRET
openssl rand -base64 32    # API_KEY_MASTER_KEY (32-byte AES-256 master key)

.env.example carries the full annotated list; .env.prod.example is the production variant. The tables below group the settings by concern.

Databases

Variable Description Default
POSTGRES_URL Async PostgreSQL connection string. Production refuses to boot on this default, on a leftover CHANGE_ME, or on a guessable password (one equal to the username, or a compose default like password) postgresql+asyncpg://maestro:maestro@localhost:5433/maestro
MONGODB_URL MongoDB connection string. Port 27018, not the stock 27017 — a natively-installed MongoDB binds loopback and beats Docker's wildcard bind, so 27017 can silently reach the wrong server. Production refuses to boot on this default or on a leftover CHANGE_ME mongodb://localhost:27018
MONGODB_DB_NAME MongoDB database name maestro
QDRANT_URL Qdrant vector DB address http://localhost:6333
QDRANT_API_KEY Qdrant API key (optional for local, and optional in production too — but a leftover CHANGE_ME is rejected at boot)

Security & auth

Variable Description Default
JWT_SECRET JWT signing secret — random and confidential, min 32 chars in production
JWT_ALGORITHM JWT signing algorithm HS256
ACCESS_TOKEN_EXPIRE_MINUTES Access token lifetime 30
REFRESH_TOKEN_EXPIRE_DAYS Refresh token lifetime 7
REFRESH_COOKIE_SECURE Secure on the refresh cookie. Unset means "on outside development" — Safari refuses a Secure cookie over http://localhost, so a hard true would break npm run dev. Production refuses to boot with it off. unset
REFRESH_COOKIE_SAMESITE strict or lax. The only CSRF control on /auth/refresh and /auth/logout; none is rejected at boot. strict
REFRESH_COOKIE_DOMAIN Empty = host-only. Naming a domain shares the session cookie with every subdomain; only for an app. / api. split under one registrable domain.
API_KEY_MASTER_KEY AES-256-GCM master key for encrypting BYOK keys (32 bytes, base64 or hex)
CORS_ORIGINS Allowed frontend origins (comma-separated) http://localhost:3000
LLM_SSRF_GUARD_ENABLED Validate custom provider endpoints (http(s), credential-free, public addresses only); disable only on a fully self-hosted stack true

Rate limiting

Variable Description Default
REDIS_URL Redis for shared sliding-window buckets; empty falls back to in-process memory (single dev worker). A leftover CHANGE_ME or a guessable password is rejected in production — it would otherwise boot and silently fall back to per-process buckets
RATE_LIMIT_ENABLED Master throttle switch. false is rejected at boot in production true
TRUST_PROXY_HEADERS true only behind a proxy that appends X-Forwarded-For (e.g. Caddy). Unset is rejected in production: neither value is safe to guess unset (means false)

TRUST_PROXY_HEADERS cuts both ways: exposed directly to the internet, a client forges the header and opens a fresh bucket per request; left false behind a proxy, every user shares the proxy's single bucket. That is why there is no default worth shipping: production refuses to boot until the variable is set explicitly, while development and the test suite treat unset as false. Code reads the resolved settings.proxy_headers_are_trusted, never the tri-state field.

Models & embeddings

Variable Description Default
FREE_MODEL_ENDPOINT Ollama OpenAI-compatible endpoint http://localhost:11434/v1
FREE_MODEL_NAME Free-tier / local model qwen3.5:9b
OLLAMA_CHAT_ENABLED Serve the local Ollama chat model; false on hosted deployments where the Ollama service only runs embeddings true
OLLAMA_NATIVE_TOOLS Use Ollama's native function calling instead of the directive fallback false
EMBEDDING_ENDPOINT Embedding endpoint; reuses FREE_MODEL_ENDPOINT when blank
EMBEDDING_MODEL_NAME RAG embedding model nomic-embed-text
EMBEDDING_DIM Embedding vector dimension 768
GEMINI_MODEL_NAME Gemini model id; the -latest alias survives model retirements — pin a stable id for deterministic behavior gemini-flash-latest
LLM_REQUEST_TIMEOUT_SECONDS Per-LLM-call read timeout 180
LLM_CONNECT_TIMEOUT_SECONDS Per-LLM-call connect timeout 10

Agent tools

Variable Description Default
WEB_SEARCH_ENABLED DuckDuckGo web-search tool true
WEB_SEARCH_MAX_RESULTS Results per query 5
WEB_SEARCH_TIMEOUT_SECONDS Per-query timeout 10
WEB_SEARCH_MAX_USES_PER_SUBTASK Searches per subtask 3
DATA_FETCH_ENABLED Data-fetch tool (Scrapling: TLS-impersonating GET → page text, or CSS-selected JSON) true
DATA_FETCH_TIMEOUT_SECONDS Per-fetch timeout 15
DATA_FETCH_MAX_USES_PER_SUBTASK Fetches per subtask 3
DATA_FETCH_ENGINE scrapling or httpx (the pre-Scrapling path: no selectors, no impersonation, but a streaming size cap) scrapling
DATA_FETCH_RENDER_ENABLED Browser tier for JS-rendered pages; needs scrapling install in the image plus ~1GB RAM. Self-host only — the tool works fully without it false
DATA_FETCH_RENDER_TIMEOUT_SECONDS Per-render timeout 45
DATA_FETCH_RENDER_MAX_CONCURRENCY Concurrent browser renders 1
REPO_INTEL_ENABLED GitHub repository intelligence for the Open Source squad. Works with no key at all (anonymous reads, 60/hour); a user's stored token raises it to 5000 true
SOCIAL_SEARCH_ENABLED X post search for the Social Listening squad. Needs the user's X key; withheld without one true
COMMUNITY_READ_ENABLED Discord / Slack / Telegram channel reading for the Community squad. Needs the user's key for that platform true
PLACES_INTEL_ENABLED Google Places lookup for the Local Market squad. Needs the user's Maps key true
CODE_EXECUTION_ENABLED Docker code sandbox. Off by default and self-host only: it needs access to the Docker daemon, so enabling it on a hosted deployment puts the host in the tool's blast radius false
CODE_EXECUTION_IMAGE Sandbox container image python:3.12-slim
CODE_EXECUTION_TIMEOUT_SECONDS Per-run timeout 30
CODE_EXECUTION_MEMORY_LIMIT / CODE_EXECUTION_CPUS Sandbox resource limits 512m / 1
CODE_EXECUTION_MAX_USES_PER_SUBTASK Runs per subtask 3

A missing service key is deliberately not fatal: the tool is withheld from the squad, the members fall back to web_search, and the answer's mandatory data-coverage section states what could not be reached. A missing brain key stops the task and tells the user.

Agent limits & execution

Variable Description Default
MAX_ITERATIONS Max steps per Subagent 10
MAX_REVIEW_ITERATIONS Reviewer ↔ Subagent loop limit 3
TASK_TIMEOUT_SECONDS Total timeout per task (whole pipeline) 1800
SUBAGENT_MAX_PARALLEL Concurrent Subagents per task 3
SUBAGENT_MAX_TOOL_CALLS Total tool calls (all kinds) per subtask 6
REVIEWER_FAIL_MODE What a reviewer crash means for the subtask: warn, approve, or reject warn
TASK_RETENTION_DAYS Mongo TTL on task sessions + agent logs; dashboard metrics cover this window 30

Payments

Variable Description Default
PAYMENT_PROVIDER Payment gateway; only mock is implemented (Luhn/BIN validation, moves no real money) mock
BILLING_ENABLED Whether paid plans are reachable. false parks them: subscribe/cancel answer 403 for everyone but admins, and every account runs on the unlimited free plan. Flip together with BILLING_LIVE in frontend/src/lib/legal/config.ts false
GRANT_ADMIN_EMAILS Comma-separated accounts python -m app.scripts.grant_admin promotes to admin (empty)

Plan prices and quotas are product constants in backend/app/core/constants.py, not environment variables.

Transactional email

Variable Description Default
EMAIL_PROVIDER console logs messages (dev / self-host — verification links are read from the log); resend sends via the Resend API console
RESEND_API_KEY Required when EMAIL_PROVIDER=resend; checked at boot in production
EMAIL_FROM From header for outgoing mail Maestro <noreply@maestro.example.com>
SITE_URL Base URL the backend uses to build verification / reset links http://localhost:3000
EMAIL_VERIFICATION_REQUIRED Soft-gates task start, API-key creation and custom-API-tool writes until the email is verified. Ships off: the default console sender delivers nothing to an inbox, so an enforced gate would lock a fresh install rather than protect it. Enable it only alongside a real sender, and flip EMAIL_VERIFICATION_LIVE in frontend/src/lib/legal/config.ts with it false

Signup abuse protection

Two protections guard the endpoints that send mail. Neither needs configuring.

The per-recipient mail budget counts sends per recipient address and is always on — rate_limit keys by caller, and every one of these endpoints lets the caller choose who receives the mail, so a caller-keyed limit measures the wrong thing. The anti-automation check (honeypot plus a server-signed form challenge) guards the two unauthenticated endpoints, /auth/register and /auth/forgot-password; a CAPTCHA provider is an optional third layer on top.

Rejections are silent by design: the endpoint answers with its normal success body, because those endpoints already guarantee a response that reveals nothing about whether an address exists. Watch maestro_abuse_rejected_total{reason} on /metrics — a silent rejection is invisible to the user, so that counter is the only place a false positive shows up.

Variable Description Default
CAPTCHA_PROVIDER none or turnstile. none is not "unprotected": the honeypot and form-challenge layers still run and no third party is contacted. There is deliberately no production guard forcing a provider — requiring one would refuse to boot every self-hosted deployment, the same trap EMAIL_VERIFICATION_REQUIRED avoids by shipping off none
CAPTCHA_SITE_KEY Cloudflare Turnstile site key. Served to the browser at runtime by GET /api/v1/auth/challenge, never baked into the frontend bundle, so turning a provider on needs no rebuild
CAPTCHA_SECRET_KEY Turnstile secret, used server-side only. Production refuses to boot if either key is empty while CAPTCHA_PROVIDER=turnstile: a provider that cannot verify fails closed on every call and stops registration silently

Turnstile verification fails closed — if Cloudflare is unreachable, the submission is refused. That costs nothing in practice: when siteverify is down the widget did not load for real users either, so they hold no token regardless, and failing open would relax the gate only for clients that never needed it.

App & observability

Variable Description Default
ENVIRONMENT production enforces strong secrets and closes Swagger development
LOG_LEVEL Application log level INFO
LOG_FORMAT text for local dev, json for structured logs in production text
SENTRY_DSN Sentry error tracking; empty disables Sentry entirely
SENTRY_TRACES_SAMPLE_RATE Tracing/APM sample rate (0.0 = off) 0.0
SENTRY_ENVIRONMENT Sentry environment tag; falls back to ENVIRONMENT
TRACING_ENABLED Per-task span tracing (Mongo trace_spans); disabled = zero overhead false
TRACE_RETENTION_DAYS TTL on stored trace spans 30
HEALTH_DETAIL_TOKEN Unlocks the per-dependency checks map on /health/ready for callers sending it as X-Health-Token; empty withholds it from everyone. Grants nothing else

Operator alerting & metrics

Self-contained: no monitoring service to run. Both alert channels empty means alerting is a silent no-op with zero egress — configuring a channel is the enable. See DEPLOYMENT.md "Monitoring" for what fires and why.

Variable Description Default
ALERT_WEBHOOK_URL Slack/Discord-compatible incoming webhook (one payload serves both). This URL is a credential — never logged or echoed in an error
ALERT_EMAIL_TO Operator address for the same alerts, delivered via EMAIL_PROVIDER
ALERT_WATCHDOG_INTERVAL_SECONDS Seconds between readiness/error-rate evaluations; 0 stops the loop, which also freezes the /metrics dependency gauges 60
ALERT_READINESS_FAILURES Consecutive failing ticks before "degraded" is declared. Recovery takes one good tick 2
ALERT_COOLDOWN_SECONDS Minimum gap between two alerts sharing a dedupe key; claimed in Redis so N workers page once 900
ALERT_ERROR_RATE_THRESHOLD 5xx share that triggers an alert. A ratio, so it means the same at any WEB_CONCURRENCY 0.05
ALERT_ERROR_RATE_WINDOW_SECONDS Rolling window the ratio is measured over 300
ALERT_ERROR_RATE_MIN_REQUESTS Volume floor below which the ratio is ignored 20
METRICS_TOKEN Unlocks GET /metrics (Prometheus text format) for callers sending it as X-Metrics-Token; empty makes the route answer 404. Deliberately separate from HEALTH_DETAIL_TOKEN
UPTIME_INTERVAL_SECONDS Poll interval for the optional uptime compose profile 60
UPTIME_FAILURES_BEFORE_ALERT Consecutive failures before the sidecar alerts 2

Deployment-specific settings — domain, TLS, backups, the purge cron — live in DEPLOYMENT.md. Architectural rationale for the non-obvious settings is in CLAUDE.md §11.