config.py boot-gate; this table mirrors the committed, value-less <dir>/.env.example files..env.example → fill the secret rows → paste into Coolify. A required var is enforced at boot: the container refuses to start (fails loud + fast) if it's missing, so a mis-set environment never reaches the first request.Two boot-gate styles: oll-core hard-fails only when APP_ENV=production (warns otherwise) and only on the two security secrets; the product services (oll-model · oll-write · foto) fail on ANY missing REQUIRED_ENV unconditionally. Several requirements are provider-conditional — only the selected provider's key is demanded, so mock/keyless runs work in CI and on a laptop. Every service reads PORT (12-factor); Coolify's upstream is 3000, so set PORT=3000 on each Coolify app or you get a 502.
Boot-gate (create_app.py _enforce_secret_boot_gate): in APP_ENV=production it refuses to boot without a JWT secret and the Stripe webhook secret; elsewhere it warns. DATABASE_URL isn't gated but, unset, Core falls back to local SQLite (crash-loops on a PaaS) — treat it as required. Billing/email need their keys to actually function. Env file: core/.env.example.
| Var | Required? | Secret? | Default | Local → Prod |
|---|---|---|---|---|
| AUTH_JWT_SECRET | yes* | secret | — | identical to specview's (= SSO); same value both |
| JWT_SECRET | alias | secret | — | legacy alias — set to the SAME value as AUTH_JWT_SECRET |
| STRIPE_WEBHOOK_SECRET | yes* | secret | — | test whsec_… → the core.oll.am live endpoint secret |
| DATABASE_URL | yes† | secret | SQLite fallback | Neon oll_core both (prod + local mirror) |
| STRIPE_SECRET_KEY | billing | secret | — | sk_test_… → rotated live sk_live_… |
| STRIPE_PRO_PRICE_ID | billing | no | — | test price → live Pro price id |
| STRIPE_PRICE_{PRODUCT}_{PLAN} | per-product | no | falls back to _PRO_PRICE_ID | e.g. STRIPE_PRICE_HEADSHOT_CHF49_ONE_TIME for foto tiers |
| RESEND_API_KEY | secret | — | re_… both | |
| FROM_EMAIL | no | no | oll.am <hello@oll.am> | a verified Resend sender |
| APP_ENV | no | no | (unset) | unset (gate warns) → production (gate hard-fails) |
| FRONTEND_URL | no | no | → SITE_URL → https://oll.am | the default product's verify + Stripe-return base |
| SITE_URL | no | no | https://oll.am | email-link base |
| DEFAULT_PRODUCT | no | no | specview | same both |
| PRODUCT_VERIFY_BASE_<P> | per-product | no | — | allow-list a non-default product's verify origin (e.g. humaniz.me) |
| PRODUCT_FRONTEND_BASE_<P> | optional | no | → PRODUCT_VERIFY_BASE_<P> | per-product Stripe-return origin |
| CORS_ORIGINS | no | no | "" (none) | comma-separated exact frontend origins |
| PORT | no | no | 5000 | 5000 local → 3000 on Coolify |
| LOG_LEVEL · SENTRY_DSN · SPEC_DOC_DIR | no | SENTRY=secret | INFO · off · /data | SENTRY_DSN off if unset; SPEC_DOC_DIR only for the SQLite fallback |
yes* = hard-required at boot only when APP_ENV=production (a warning otherwise). yes† = not boot-gated but required in practice (SQLite fallback crash-loops on a PaaS). billing/email = the service boots without it but that feature 500s / no-ops. Never set SKIP_AUTH — it's a dev-only auth bypass.
Boot-gate (app.py → config.REQUIRED_ENV): fails on ANY missing required var. The service token is always required; the provider key is provider-conditional — only the LLM_PROVIDER default's credential is demanded (so ollama needs no cloud key = "pay once"). Env file: services/oll-model/.env.example.
| Var | Required? | Secret? | Default | Local → Prod |
|---|---|---|---|---|
| OLL_MODEL_SERVICE_TOKEN | yes | secret | — | shared X-Service-Token; must match every caller |
| GROQ_API_KEY | if groq | secret | — | gsk_… — required when LLM_PROVIDER=groq (default) |
| ANTHROPIC_API_KEY | if claude | secret | — | sk-ant-… — required only when LLM_PROVIDER=claude |
| OLLAMA_BASE_URL | if ollama | no | http://localhost:11434 | required only when LLM_PROVIDER=ollama |
| LLM_PROVIDER | no | no | groq | groq | claude | ollama (per-call override too) |
| GROQ_MODEL | no | no | llama-3.1-8b-instant | same both |
| GROQ_BASE_URL | no | no | https://api.groq.com/openai/v1 | same both |
| ANTHROPIC_MODEL | no | no | claude-opus-4-8 | same both |
| OLLAMA_MODEL | no | no | qwen2.5:0.5b | same both |
| OLLAMA_ALLOWED_MODELS | no | no | llama3.2:1b,qwen2.5:0.5b | caps loadable models; "" = allow any (dev only) |
| LLM_TIMEOUT | no | no | 60 | seconds; never unbounded |
| LLM_MAX_TOKENS | no | no | 2000 | default output ceiling |
| LLM_MAX_TOKENS_CEILING | no | no | 4000 | absolute clamp |
| MAX_INPUT_CHARS | no | no | 24000 | over this = 413 |
| CORS_ORIGINS | no | no | "" (none) | internal-only — leave empty |
| PORT | no | no | 5003 | 5003 local → 3000 on Coolify |
Boot-gate (app.py → REQUIRED_ENV): fails on ANY of the three below. A pure client — zero auth/Stripe/email/model-SDK code; the gateway is the sole model path (no direct-vendor fallback), so it's required at boot. Env file: services/oll-write/.env.example.
| Var | Required? | Secret? | Default | Local → Prod |
|---|---|---|---|---|
| CORE_BASE_URL | yes | no | — | http://oll-core:5000 → https://core.oll.am |
| OLL_MODEL_BASE_URL | yes | no | — | http://oll-model:5003 → https://oll-model.oll.am |
| OLL_MODEL_SERVICE_TOKEN | yes | secret | — | must match oll-model's token |
| CORE_HTTP_TIMEOUT | no | no | 15 | seconds |
| ME_CACHE_TTL | no | no | 30 | seconds; /me stale-while-revalidate window |
| OLL_MODEL_HTTP_TIMEOUT | no | no | 65 | seconds; covers oll-model's LLM_TIMEOUT |
| MAX_INPUT_CHARS | no | no | 12000 | hard truncation, all plans |
| FREE_MAX_CHARS | no | no | 1200 | free-tier cap → over = 402 |
| FREE_REQUEST_LIMIT | no | no | 5 | in-proc stub; 0 disables |
| CORS_ORIGINS | no | no | "" (none) | the product frontend origin (e.g. humaniz.me) |
| PORT | no | no | 5002 | 5002 local → 3000 on Coolify |
Boot-gate (app.py → REQUIRED_ENV): CORE_BASE_URL + FOTO_PUBLIC_BASE_URL always; the Replicate token + Resend key are provider-conditional, so the default mock providers run keyless in CI and on a laptop. Persists to on-disk SQLite (mount a /data volume). Env file: services/foto/.env.example.
| Var | Required? | Secret? | Default | Local → Prod |
|---|---|---|---|---|
| CORE_BASE_URL | yes | no | — | http://oll-core:5000 → https://core.oll.am |
| FOTO_PUBLIC_BASE_URL | yes | no | — | http://localhost:5001 → https://foto.oll.am (Stripe return) |
| REPLICATE_API_TOKEN | if replicate | secret | — | r8_… — required only when FOTO_PROVIDER=replicate |
| RESEND_API_KEY | if resend | secret | — | re_… — required only when EMAIL_PROVIDER=resend |
| FOTO_PROVIDER | no | no | mock | mock (keyless) → replicate (real) |
| EMAIL_PROVIDER | no | no | mock | mock (no-op) → resend (real) |
| FROM_EMAIL | no | no | oll.am <noreply@oll.am> | a verified sender when EMAIL_PROVIDER=resend |
| CORE_SERVICE_JWT | optional | secret | "" (empty) | only the authed send_email path; guest money path sends none |
| FOTO_DB_PATH | no | no | /data/foto.db | mount a volume at /data for durability |
| CORE_HTTP_TIMEOUT | no | no | 15 | seconds |
| REPLICATE_TIMEOUT · _POLL_INTERVAL | no | no | 600 · 5 | seconds; LoRA train+sample is slow |
| CORS_ORIGINS | no | no | "" (none) | set explicitly — /fulfill returns paid image URLs |
| PORT | no | no | 5001 | 5001 local → 3000 on Coolify |
| LORA_* · SAMPLE_* · FLUX_LORA_TRAINER · REPLICATE_USERNAME/HARDWARE | no | no | wardrobai + spec defaults | the real flux-LoRA pipeline params; mock ignores them |
<dir>/.env.example → set the secret rows → set PORT=3000 for Coolify → deploy.DATABASE_URL (Neon oll_core), and Resend. Set APP_ENV=production so the boot-gate protects you.CORE_BASE_URL (and, for write, OLL_MODEL_BASE_URL) at the deployed services; share the model service token. foto runs keyless on mock providers until you flip FOTO_PROVIDER=replicate..env.example files, verified against each config.py boot-gate · pairs with CI/CD & Environments