oll.am · Service Env Matrix · reference · 2026-07-03

Every var to stand a service up.

The single place to look when you create a new environment or a new Coolify app: for each deployable service — what's required (boot-gated), what's a secret, what the default is, and local vs prod. The source of truth is each service's config.py boot-gate; this table mirrors the committed, value-less <dir>/.env.example files.

How to read this

Copy the service's .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.

The four deployable services

oll-coreauth · billing · email — the frozen spine · Neon oll_core · :5000
oll-modelstateless model-call gateway · provider switch · :5003
products call oll-core + oll-model over HTTP
oll-writewriting product · calls oll-core (identity) + oll-model (text) · :5002
fotoheadshot product · calls oll-core (billing) + Replicate · SQLite · :5001

oll-core — auth · billing · email (the frozen spine)

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.

VarRequired?Secret?DefaultLocal → Prod
AUTH_JWT_SECRETyes*secretidentical to specview's (= SSO); same value both
JWT_SECRETaliassecretlegacy alias — set to the SAME value as AUTH_JWT_SECRET
STRIPE_WEBHOOK_SECRETyes*secrettest whsec_… → the core.oll.am live endpoint secret
DATABASE_URLyes†secretSQLite fallbackNeon oll_core both (prod + local mirror)
STRIPE_SECRET_KEYbillingsecretsk_test_… → rotated live sk_live_…
STRIPE_PRO_PRICE_IDbillingnotest price → live Pro price id
STRIPE_PRICE_{PRODUCT}_{PLAN}per-productnofalls back to _PRO_PRICE_IDe.g. STRIPE_PRICE_HEADSHOT_CHF49_ONE_TIME for foto tiers
RESEND_API_KEYemailsecretre_… both
FROM_EMAILnonooll.am <hello@oll.am>a verified Resend sender
APP_ENVnono(unset)unset (gate warns) → production (gate hard-fails)
FRONTEND_URLnono→ SITE_URL → https://oll.amthe default product's verify + Stripe-return base
SITE_URLnonohttps://oll.amemail-link base
DEFAULT_PRODUCTnonospecviewsame both
PRODUCT_VERIFY_BASE_<P>per-productnoallow-list a non-default product's verify origin (e.g. humaniz.me)
PRODUCT_FRONTEND_BASE_<P>optionalno→ PRODUCT_VERIFY_BASE_<P>per-product Stripe-return origin
CORS_ORIGINSnono"" (none)comma-separated exact frontend origins
PORTnono50005000 local → 3000 on Coolify
LOG_LEVEL · SENTRY_DSN · SPEC_DOC_DIRnoSENTRY=secretINFO · off · /dataSENTRY_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.

oll-model — the model-call gateway

Boot-gate (app.pyconfig.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.

VarRequired?Secret?DefaultLocal → Prod
OLL_MODEL_SERVICE_TOKENyessecretshared X-Service-Token; must match every caller
GROQ_API_KEYif groqsecretgsk_… — required when LLM_PROVIDER=groq (default)
ANTHROPIC_API_KEYif claudesecretsk-ant-… — required only when LLM_PROVIDER=claude
OLLAMA_BASE_URLif ollamanohttp://localhost:11434required only when LLM_PROVIDER=ollama
LLM_PROVIDERnonogroqgroq | claude | ollama (per-call override too)
GROQ_MODELnonollama-3.1-8b-instantsame both
GROQ_BASE_URLnonohttps://api.groq.com/openai/v1same both
ANTHROPIC_MODELnonoclaude-opus-4-8same both
OLLAMA_MODELnonoqwen2.5:0.5bsame both
OLLAMA_ALLOWED_MODELSnonollama3.2:1b,qwen2.5:0.5bcaps loadable models; "" = allow any (dev only)
LLM_TIMEOUTnono60seconds; never unbounded
LLM_MAX_TOKENSnono2000default output ceiling
LLM_MAX_TOKENS_CEILINGnono4000absolute clamp
MAX_INPUT_CHARSnono24000over this = 413
CORS_ORIGINSnono"" (none)internal-only — leave empty
PORTnono50035003 local → 3000 on Coolify

oll-write — the writing product

Boot-gate (app.pyREQUIRED_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.

VarRequired?Secret?DefaultLocal → Prod
CORE_BASE_URLyesnohttp://oll-core:5000https://core.oll.am
OLL_MODEL_BASE_URLyesnohttp://oll-model:5003https://oll-model.oll.am
OLL_MODEL_SERVICE_TOKENyessecretmust match oll-model's token
CORE_HTTP_TIMEOUTnono15seconds
ME_CACHE_TTLnono30seconds; /me stale-while-revalidate window
OLL_MODEL_HTTP_TIMEOUTnono65seconds; covers oll-model's LLM_TIMEOUT
MAX_INPUT_CHARSnono12000hard truncation, all plans
FREE_MAX_CHARSnono1200free-tier cap → over = 402
FREE_REQUEST_LIMITnono5in-proc stub; 0 disables
CORS_ORIGINSnono"" (none)the product frontend origin (e.g. humaniz.me)
PORTnono50025002 local → 3000 on Coolify

foto — the headshot product

Boot-gate (app.pyREQUIRED_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.

VarRequired?Secret?DefaultLocal → Prod
CORE_BASE_URLyesnohttp://oll-core:5000https://core.oll.am
FOTO_PUBLIC_BASE_URLyesnohttp://localhost:5001https://foto.oll.am (Stripe return)
REPLICATE_API_TOKENif replicatesecretr8_… — required only when FOTO_PROVIDER=replicate
RESEND_API_KEYif resendsecretre_… — required only when EMAIL_PROVIDER=resend
FOTO_PROVIDERnonomockmock (keyless) → replicate (real)
EMAIL_PROVIDERnonomockmock (no-op) → resend (real)
FROM_EMAILnonooll.am <noreply@oll.am>a verified sender when EMAIL_PROVIDER=resend
CORE_SERVICE_JWToptionalsecret"" (empty)only the authed send_email path; guest money path sends none
FOTO_DB_PATHnono/data/foto.dbmount a volume at /data for durability
CORE_HTTP_TIMEOUTnono15seconds
REPLICATE_TIMEOUT · _POLL_INTERVALnono600 · 5seconds; LoRA train+sample is slow
CORS_ORIGINSnono"" (none)set explicitly — /fulfill returns paid image URLs
PORTnono50015001 local → 3000 on Coolify
LORA_* · SAMPLE_* · FLUX_LORA_TRAINER · REPLICATE_USERNAME/HARDWAREnonowardrobai + spec defaultsthe real flux-LoRA pipeline params; mock ignores them

Turnkey checklist for a new environment

Per service: copy <dir>/.env.example → set the secret rows → set PORT=3000 for Coolify → deploy.
  1. oll-core first — it's the spine. Needs the JWT secret (identical to specview), the Stripe secret + webhook secret, DATABASE_URL (Neon oll_core), and Resend. Set APP_ENV=production so the boot-gate protects you.
  2. oll-model — one service token + the default provider's key (Groq out of the box). Internal-only; no CORS.
  3. oll-write / foto — point 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.
  4. Everything else has a code default — leave it unless you're tuning.
oll.am · service env matrix · reference · 2026-07-03 · mirrors the committed .env.example files, verified against each config.py boot-gate · pairs with CI/CD & Environments
Source of truth = the service boot-gates (create_app.py · app.py + config.py REQUIRED_ENV). Update the .env.example + this page together.