---
title: Configuration
path: reference/configuration
status: published
---

# Configuration

ScaiControl reads its configuration from environment variables via `backend/src/scaicontrol/config.py` (Pydantic `BaseSettings`). The complete set:

## Core

| Variable | Default | Purpose |
|---|---|---|
| `DATABASE_URL` | `mysql+asyncmy://…` | Async DB URL. Production points at a MariaDB Galera cluster; dev uses `aiosqlite` |
| `REDIS_URL` | `redis://localhost:6379/0` | Queue + cache + short-lived state |
| `LOG_LEVEL` | `INFO` | Backend log verbosity |
| `PORTAL_URL` | `http://localhost:5173` | Used in invoice PDFs (logo + asset URLs) and email links |
| `API_URL` | `http://localhost:8000` | Self-reference for callbacks |

## S3 (PDFs, uploads)

| Variable | Purpose |
|---|---|
| `S3_BUCKET` | Bucket name |
| `S3_REGION` | AWS region (or compatible service region) |
| `S3_ENDPOINT_URL` | Optional; set for MinIO / non-AWS providers |
| `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` | Credentials. Omit on AWS to use instance role |
| `S3_PREFIX` | Key prefix for ScaiControl objects (e.g. `uploads`) |
| `UPLOAD_MAX_SIZE_MB` | `50` | Per-file upload limit |

## ScaiKey (identity)

| Variable | Purpose |
|---|---|
| `SCAIKEY_ISSUER` | OIDC issuer URL |
| `SCAIKEY_CLIENT_ID` | Confidential client ID for the portal |
| `SCAIKEY_CLIENT_SECRET` | Client secret (or vault path below) |
| `SCAIKEY_CLIENT_SECRET_VAULT_PATH` | ScaiVault path for the secret |
| `SCAIKEY_WEBHOOK_SECRET` | Inbound webhook signing secret |
| `SCAIKEY_WEBHOOK_SECRET_VAULT_PATH` | Or via vault |
| `SCAIKEY_JWKS_CACHE_TTL` | `3600` (seconds) for JWKS rotation |

## Payment providers

### Stripe
| Variable | Purpose |
|---|---|
| `STRIPE_API_KEY` / `STRIPE_API_KEY_VAULT_PATH` | Secret key |
| `STRIPE_PUBLISHABLE_KEY` | Frontend-visible publishable key |
| `STRIPE_WEBHOOK_SECRET` / `STRIPE_WEBHOOK_SECRET_VAULT_PATH` | Webhook signing secret |

### Mollie
| Variable | Purpose |
|---|---|
| `MOLLIE_API_KEY` / `MOLLIE_API_KEY_VAULT_PATH` | API key |
| `MOLLIE_WEBHOOK_SECRET` / `MOLLIE_WEBHOOK_SECRET_VAULT_PATH` | Inbound webhook secret |

### Crypto (Coinbase Commerce)
| Variable | Purpose |
|---|---|
| `CRYPTO_API_KEY` / `CRYPTO_API_KEY_VAULT_PATH` | Coinbase Commerce API key |
| `CRYPTO_WEBHOOK_SECRET` / `CRYPTO_WEBHOOK_SECRET_VAULT_PATH` | Webhook secret |
| `CRYPTO_ACCEPTED_COINS` | `BTC,ETH,USDC,USDT` (comma-separated) |
| `CRYPTO_SETTLEMENT_CURRENCY` | `EUR` |

## ScaiSend (email)

| Variable | Purpose |
|---|---|
| `SCAISEND_API_URL` | `https://scaisend.scailabs.ai/` |
| `SCAISEND_API_KEY` | Inline API key (dev) |
| `SCAISEND_API_KEY_VAULT_PATH` | Vault path (production) |

ScaiControl reads vault path first; falls back to inline. Empty config → email sends become no-ops (logged warning, no error).

## ScaiVault (secrets)

| Variable | Purpose |
|---|---|
| `SCAIVAULT_API_URL` | ScaiVault base URL |
| `SCAIVAULT_API_KEY` | Auth key |

## Service registry / heartbeats

| Variable | Default | Purpose |
|---|---|---|
| `REGISTRY_HEARTBEAT_MONITOR_INTERVAL` | `60` | Monitor interval in seconds |
| `REGISTRY_HEARTBEAT_GRACE_MULTIPLIER` | `3` | Grace period as multiplier of the service's own heartbeat_interval_seconds |
| `REGISTRY_HEARTBEAT_DEGRADED_THRESHOLD` | `3` | Consecutive failures → degraded |
| `REGISTRY_HEARTBEAT_UNREACHABLE_THRESHOLD` | `10` | Consecutive failures → unreachable |
| `AUTO_APPROVED_APP_IDS` | first-party slugs | Services that skip pending-approval gate |

## Operator branding

| Variable | Purpose |
|---|---|
| `OPERATOR_LEGAL_NAME` | E.g. `ScaiLabs B.V.` — used in fallback email "From" name and seller snapshots when partner config is missing |

## Visma e-Accounting (optional)

| Variable | Purpose |
|---|---|
| `VISMA_EACCOUNTING_CLIENT_ID` / `VISMA_EACCOUNTING_CLIENT_SECRET` (+ vault path) | OAuth |

## Vault precedence

For every `*_VAULT_PATH` + plain-secret pair, ScaiControl resolves in this order:

1. Vault path → ScaiVault lookup → returned value
2. Plain env variable → returned value as-is
3. Neither → the integration is "unconfigured"; calls degrade gracefully (warning log, no crash)

This lets you run the same image in dev (inline secrets in `.env`) and production (vault paths) without code changes.

## Health & readiness

GET `/api/v1/health` returns `{"status":"ok"}`. No DB roundtrip; suitable for liveness probes. There's no separate readiness endpoint today — for now the DB connection is checked lazily on first query.
