Configuration
Every environment variable read by the SAT API, the web build, the sat CLI and the runner, with defaults and production requirements.
SAT is configured entirely through environment variables. There is no config file for the API. This page lists every variable that the running code reads. Variables are read once at import time unless noted, so changing one requires restarting the API (on Cloud Run, gcloud run services update creates a new revision).
In production, set the API's variables on the service itself and take DATABASE_URL, JWT_SECRET_KEY and SMTP_PASSWORD from a secret store (for example Secret Manager), never as plain values. See Architecture for the deployment shape.
API
Read by the FastAPI app (apps/api, entry point main:app).
Core
| Variable | Default | Required in prod | Description |
|---|---|---|---|
DATABASE_URL | postgresql://sat_user:sat_password@localhost:5432/sat_db | Yes (secret) | SQLAlchemy URL of the PostgreSQL database. Also used by Alembic. Read in database/connection.py. |
JWT_SECRET_KEY | dev-only-insecure-jwt-secret, only in development environments | Yes (secret) | HS256 signing key for access and refresh tokens. The built-in key is used only when APP_ENV (or, if unset, ENVIRONMENT) is development, dev, local or test, ignoring case and surrounding spaces; unset counts as development. With any other value and no JWT_SECRET_KEY, the API refuses to start. Generate at least 48 random bytes, hex-encoded, for example openssl rand -hex 48. |
APP_ENV | unset | Yes | Any value other than development, dev, local or test (case-insensitive) turns on the JWT_SECRET_KEY requirement. Exactly production stops the mailer from logging email bodies when SMTP is not configured. Takes precedence over ENVIRONMENT for those two checks. |
ENVIRONMENT | development | Yes | Reported by /health and /. When exactly production, unhandled-error responses omit the exception text. Used for the two checks above when APP_ENV is unset. Set to the same value as APP_ENV. |
DEBUG | false | No (keep false) | true serves Swagger UI at /docs, ReDoc at /docs/redoc, the schema at /openapi.json and the route list at /api/debug/routes. start_api_local.py sets it to true. |
PORT | 8080 | Set by Cloud Run | Port uvicorn listens on. The Docker image sets PORT=8080 and runs uvicorn main:app --host 0.0.0.0 --port ${PORT}. |
API_HOST | 0.0.0.0 | No | Bind address, only when you run python main.py directly. |
API_PORT | 8000 | No | Port only when you run python api/main_api.py directly. Not used by the Docker image or main.py. |
DB_ECHO | false | No | true logs every SQL statement. |
Three checks, three comparisons
The JWT_SECRET_KEY check fails closed: only development, dev, local and test (any case) may use the public development key, so APP_ENV=prod or ENVIRONMENT=Production without a secret stops the API at startup with JWT_SECRET_KEY must be set. The other two checks compare literally: the mailer keeps email bodies out of the log only when APP_ENV (or ENVIRONMENT) is exactly production, and error responses drop the exception text only when ENVIRONMENT is exactly production. Use the exact value production (or staging) for both variables.
HTTP
| Variable | Default | Required in prod | Description |
|---|---|---|---|
ALLOWED_ORIGINS | * | Yes | Comma-separated CORS origins, for example https://sat.example.com,https://<site>.web.app,https://<site>.firebaseapp.com. List every origin the web app is served from. The browser calls the API's direct URL for live events, so the web origin must be listed. Also the fallback for WEB_APP_URL. |
ALLOWED_HOSTS | unset (no check) | No | Comma-separated Host header allow-list. Leave unset in Cloud Run: requests arrive as *.run.app, through Firebase Hosting, and on tagged revision URLs, and a fixed list breaks deploys. |
GOOGLE_OAUTH_CLIENT_ID | unset | No | The OAuth client ID (…apps.googleusercontent.com) that turns on Sign in with Google. Unset, the feature is off: GET /api/auth/config returns google_client_id: null and the web app hides the button. It is public (an audience, not a secret); there is no client secret. Use a separate client per environment and per product. See Sign in with Google. |
WEB_APP_URL | first http(s) entry of ALLOWED_ORIGINS, else http://localhost:4200 | Yes | Base URL for links in emails: <WEB_APP_URL>/reset-password?token=..., for example https://sat.example.com. |
Runs and events
| Variable | Default | Required in prod | Description |
|---|---|---|---|
RUN_LEASE_SECONDS | 300 | No | A running run whose last heartbeat (or start) is older than this is failed with "lease expired" by the next claim, POST /runs/expire-stale or sat scheduler. Returned to runners as lease_seconds. Keep it well above the runner's 30-second heartbeat. |
EVENTS_BACKEND | memory | No | memory: in-process bus, one instance only. redis: Redis pub/sub on channel sat:company:<company_id>:events, required before running more than one instance. Any other value means memory. |
REDIS_URL | redis://localhost:6379/0 | Only with EVENTS_BACKEND=redis | Redis for the event bus. database/connection.py also builds a connection pool from it at import, but connects only when used, and only legacy routes use it. |
Used for password-reset links. When SMTP_HOST, SMTP_USER and SMTP_PASSWORD are not all set, no email is sent: outside production the message (including the link) is written to the API log; in production only a warning is logged.
| Variable | Default | Required in prod | Description |
|---|---|---|---|
SMTP_HOST | unset | For password reset | SMTP server, for example smtp.gmail.com. |
SMTP_PORT | 587 | No | SMTP port. |
SMTP_SECURE | false | No | true uses implicit TLS (port 465). Otherwise STARTTLS. |
SMTP_USER | unset | For password reset | SMTP login. |
SMTP_PASSWORD | unset | For password reset | SMTP password. Store it in your secret store, never as a plain env var. |
MAIL_FROM | Smart Agent Teams <SMTP_USER> | No | From header. |
To set it up on Cloud Run with Secret Manager (replace the placeholders with your project, service, region and the service's runtime service account):
printf '%s' '<app password>' | gcloud secrets create <smtp-secret> --project <project> --data-file -
gcloud secrets add-iam-policy-binding <smtp-secret> --project <project> \
--member "serviceAccount:<runtime service account>" \
--role roles/secretmanager.secretAccessor
gcloud run services update <service> --project <project> --region <region> \
--update-secrets SMTP_PASSWORD=<smtp-secret>:latest \
--update-env-vars 'SMTP_HOST=smtp.gmail.com,SMTP_PORT=587,SMTP_USER=<sending account>,MAIL_FROM=Smart Agent Teams <no-reply@example.com>'The secret must be readable by the service's runtime service account.
Not read by the running API
apps/api/config.py defines a Settings class with variables such as SECRET_KEY, APP_NAME, LOG_LEVEL, API_BASE_URL, GCP_PROJECT_ID and MAX_CONCURRENT_AGENTS. No module that main:app loads imports it, so setting those variables has no effect. LOG_LEVEL is read only by start_api_local.py, to set uvicorn's log level locally.
Production values at a glance
| Variable | Recommended value |
|---|---|
DATABASE_URL | From your secret store |
JWT_SECRET_KEY | From your secret store |
APP_ENV | production (or staging) |
ENVIRONMENT | Same as APP_ENV |
ALLOWED_ORIGINS | Every origin the web app is served from |
WEB_APP_URL | The web app's public URL |
PORT | 8080 (set by Cloud Run and the image) |
EVENTS_BACKEND | Unset (memory) with one instance; redis before you scale out |
| SMTP | Set by hand; see Email |
Web build
Vite compiles these into the browser bundle at build time. Everything here is public. Building with --mode production or --mode staging loads apps/web/.env.production or apps/web/.env.staging. Turborepo passes every VITE_* variable through to the build and includes it in the cache key.
| Variable | Default | Production value | Description |
|---|---|---|---|
VITE_API_URL | empty (same origin) | empty | Base URL for REST calls. Empty means the page's own origin, which Firebase Hosting rewrites to the API for /api/** and /health. |
VITE_EVENTS_ORIGIN | empty (same origin) | your Cloud Run service URL | Origin of the SSE stream /api/companies/<id>/events. Firebase Hosting buffers rewritten responses, so production points this at Cloud Run directly. |
Local development server (bun run dev, Vite on port 4200):
| Variable | Default | Description |
|---|---|---|
SAT_API_URL | http://localhost:8080 | Target of the dev server's proxy for /api and /health. Read in apps/web/vite.config.ts. |
sat CLI
Read by apps/cli. Flags win over environment variables, which win over the stored profile. See the CLI reference.
| Variable | Default | Description |
|---|---|---|
SAT_PROFILE | the config's defaultProfile, else local | Profile to use. Built in: local (http://localhost:8080), staging (https://sat-yarlis-staging.web.app), prod (https://app.yarlis.com, events at the Cloud Run URL). |
SAT_API_URL | the profile's URL | API base URL. Setting it also drops the profile's events URL. |
SAT_EVENTS_URL | the profile's events URL, else the API URL | Origin for the SSE stream. Set it when the API URL is behind a proxy that buffers (Firebase Hosting). |
SAT_COMPANY | the profile's stored company | Company name, task prefix or id. |
SAT_API_KEY | unset | Bearer API key for this process. Takes precedence over stored credentials and is never written to disk. |
SAT_TOKEN | unset | Bearer access token for this process, used when SAT_API_KEY is unset. Never stored. |
SAT_CONFIG_DIR | $XDG_CONFIG_HOME/sat, else ~/.config/sat | Where config.json (and credentials.json for the file store) live. Files are written with mode 0600. |
SAT_CREDENTIAL_STORE | auto | auto uses the macOS Keychain or Linux Secret Service when available, else a file. keychain behaves like auto. file forces credentials.json. |
SAT_DEBUG | unset | Print stack traces on errors. |
NO_COLOR | unset | Disable colours. |
XDG_CONFIG_HOME | ~/.config | Base for the default config directory. |
Runner
Read by sat runner start and passed to agent processes.
| Variable | Default | Description |
|---|---|---|
SAT_WORKDIR | $XDG_DATA_HOME/sat/workspaces, else ~/.local/share/sat/workspaces | Root of run workspaces (repository clones, worktrees, agent folders). --workdir overrides it. |
XDG_DATA_HOME | ~/.local/share | Base for the default workspace root. |
SAT_CODEX_PRICE_IN | unset | Dollars per million input tokens for Codex runs. Codex does not report cost, so unless both price variables are set its runs record cost_cents of 0 and do not count against budgets. |
SAT_CODEX_PRICE_OUT | unset | Dollars per million output tokens for Codex runs. |
The runner requires an API key (sat_live_...), from SAT_API_KEY or stored with sat apikey create <name> --store. It refuses to start on a password session.
Set by the runner for agent processes
Each agent process gets a clean sat environment so it can call the CLI as itself. These values override anything inherited.
| Variable | Value |
|---|---|
PATH | A sat shim directory, then the runner's PATH |
SAT_API_KEY | The runner's API key |
SAT_TOKEN, SAT_PROFILE | Empty |
SAT_CONFIG_DIR | <workdir>/.sat-agent-config (empty, mode 0700), so the agent cannot read stored credentials |
SAT_CREDENTIAL_STORE | file |
SAT_API_URL, SAT_EVENTS_URL | The runner's resolved URLs |
SAT_RUN_ID | The run id. sat adds it to task, comment and subtask writes, which attributes them to the agent. |
SAT_AGENT_ID, SAT_AGENT_NAME | The agent |
SAT_COMPANY | The company id |
SAT_TASK | The task key, when the run has a task |