Configuration

Every environment variable and runtime constant lives in one place: config/config.go. Change it once, every feature picks up the new value.

Environment variables

Secrets can come from the environment directly, or — for local development — from an age-encrypted file in ~/.secrets/ decrypted into the process environment at boot. Production does not use age: the secrets file is rendered from GitHub Actions secrets on every deploy. Full explanation, including the two-file layout (.env.age is read, .env is the plaintext working copy): Getting started → Secrets setup.

Core

VariableDefaultPurpose
PORT8080HTTP listen port
HOST0.0.0.0HTTP bind address
ENVIRONMENTdevelopmentAnything other than production selects dev mode
APP_NAMEbinary nameProject name (secrets scope); never empty
LOG_LEVELINFOslog level
DATA_DIRdataRoot for runtime data
TURSO_DB_PATHdata/app.dbTurso Database file for the flat stack (used when TURSO_DATABASE_URL is empty)
CHI_ADDRunsetListen-address override for the flat stack. Unset = HOST:PORT. cmd/web honors it since the flat-stack port (it used to be the staged-seam flag); cmd/desktop sets it itself from HOST/PORT

Skin

VariableDefaultPurpose
UI_SKINdaisyuiActive UI skin: daisyui or basecoat. Can also be overridden per request with ?skin=. Unknown values fall back to DaisyUI with a warning.

Realtime and workflows

VariableDefaultPurpose
NATS_ENABLEDtrueBoot NATS JetStream for multi-instance broadcast. false disables the engine; consumers handle nil.
NATS_STORE_DIRunder DATA_DIRJetStream storage directory
NATS_LEAFNODE_URLunsetWhen set, the desktop boots as a NATS Leaf Node replicating against your central server. Offline edits replay on reconnect.
DAGNATS_ENABLEDtrueBoot the DagNats durable workflow engine
DAGNATS_HTTP_ADDR127.0.0.1:8090DagNats HTTP API + console (loopback-only; browsers reach the console through the /console/ reverse proxy on the app mux)
DAGNATS_NATS_PORT4222Shared embedded JetStream port (DagNats boots it; the whiteboard SyncWorker attaches)
DAGNATS_STORE_DIRunder DATA_DIRDagNats store directory
DAGNATS_TRIGGER_BOOTSTRAPtrueSeed one disabled placeholder trigger when the trigger bucket is empty. Workaround for an upstream DagNats v0.0.24 bug — without it the trigger console cannot create the first trigger. See the workaround page before disabling or removing.
DAGNATS_GREET_PACING1500msHow long the onboarding greet step pauses before completing. Exists only so a human watching the stepper can read the "greeting" phase — it is deliberate product latency, so tune it rather than delete it. A non-positive or unparseable value falls back to 1500ms. Set 0-ish (e.g. 1ms) for automated runs.

Persistence

VariableDefaultPurpose
OFFLINE_SYNC_ENABLEDtrueHybrid offline sync. false disables the NATS CRUD proxy and the Service Worker offline queue — zero code paths traversed.
COLLAB_SNAPSHOTStursoCRDT snapshot store behind whiteboard/notes. memory keeps snapshots process-local (draw without persistence — docs start empty after restart). Unknown values fail closed at boot.

LLM

VariableDefaultPurpose
GOAI_API_KEYunsetAny OpenAI-compatible provider key. Unset → the AI suggest route is not registered and the button is hidden.
GOAI_BASE_URLhttps://api.openai.com/v1OpenAI-compatible base URL
GOAI_MODELgpt-4o-miniDefault model
SIMULATE_LLManything but falseIn-process fake LLM scripting 500 → retry → slow → 200, so you can watch the retry toasts without a provider key. Enabled unless explicitly set to false — including in production

Review loop (jev judge)

VariableDefaultPurpose
JEV_API_URLSimpleJev keyless demo (.../v1/classifier)Any jev-compatible endpoint (POST {state,model,questions}): TypeSafe, self-hosted SimpleJev, tensai. Bare hosts expand to /v1/systemone
JEV_MODELSimpleJev demo classifierJudge model ID served by the endpoint
JEV_API_KEYunsetJudge credential (age-encrypted secrets file). Empty = keyless demo, no Authorization header
JEV_TIMEOUT10sPer-attempt judge HTTP timeout
JEV_CONFIDENCE_THRESHOLD0.7Below this the verdict escalates to a human; recalibrate per backend (see Review loop)
JEV_ROUTER_ENABLEDfalseAsk AI only: let the same jev-compatible endpoint choose which catalog component an answer leads with. Off = byte-identical prompt to the catalog-only one
JEV_ROUTER_CONFIDENCE_THRESHOLD0.7Ask AI only: below this the pick is ignored and the whole catalog is offered

The Ask router (JEV_ROUTER_*) is opt-in and reuses the endpoint above — one jev-compatible API serves the judge and the router. Turning it on adds a leading hint to the prompt; it never removes a component from what the model may emit, and every failure (endpoint down, low confidence, a label outside the catalog) falls back to the full catalog, so Ask answers exactly as it did before. See Ask AI.

AI credits + BYOK

All dormant unless CREDITS_ENABLED=true.

VariableDefaultPurpose
CREDITS_ENABLEDfalseMaster switch for the ai-credits plugin
CREDITS_MONTHLY_CREDITS0Monthly entitlement per user
CREDITS_DEFAULT_MODEexplicitBilling mode applied when a request does not name one
CREDITS_ENC_KEYunset64 hex chars (openssl rand -hex 32). Encrypts user BYOK keys at rest. Anything else fails closed at BYOK configuration.
CREDITS_MODELGOAI_MODEL, else gpt-4o-miniModel used for priced managed calls
CREDITS_PRICING_FILEunsetPath to a pricing table
BYOK_PROVIDERSunsetopenai=https://api.openai.com/v1,groq=https://api.groq.com/openai/v1
STRIPE_SECRET_KEYunsetStripe top-ups
STRIPE_WEBHOOK_SECRETunsetStripe webhook signature verification
STRIPE_SUCCESS_URL / STRIPE_CANCEL_URLunsetRedirect targets after top-up

With CREDITS_ENABLED=true, authenticated users get GET /api/credits, the real Todo Suggest path is metered in managed mode, and the app exposes POST /api/ai/request. When both BYOK vars are present, POST /api/byok/{provider}/{path...} injects the user's encrypted key server-side and records upstream JSON/SSE token usage with billing_mode=byok and credits_charged=0.

The relay trusts the authenticated session user stamped by the app. Do not expose X-Auth-User from an external proxy.

Auth and admin

VariableDefaultPurpose
AUTH_ENC_KEYauto-created totp_seal.key64 hex chars (openssl rand -hex 32). Seals TOTP secrets at rest (XChaCha20-Poly1305). Unset auto-creates a 0600 key file in the data dir (no operator action; backups must handle it like any secret). Set-but-malformed fails the boot.
ADMIN_UNLOCK_TOKENunsetMaster-password token for the "Clear all" form. When present (in the age-encrypted secrets file), the todo UI shows the form; the handler compares constant-time.
RBAC_ENABLEDfalseEnforce the embedded Casbin policy on every request (deny = 403 + warn log). false is pass-through with identical wiring. When on: anonymous reaches only the public surface (landing, login, auth endpoints, static, health); users reach everything except /config; admins reach all.
RBAC_PERMISSIVEfalseAudit-before-enforce rollout: would-be denies are warn-logged (permissive=1) and the request proceeds. Set for one deploy after adding a feature's paths, then turn off. Only meaningful with RBAC_ENABLED=true. See RBAC.
RBAC_POLICY_SOURCEembeddedWhere the Casbin rules come from: embedded reads the compiled-in policy.csv; db loads the casbin_rule table (seeded from the embedded policy on first boot, then editable at runtime). Any other value falls back to embedded.
ADMIN_EMAILSunsetComma-separated addresses promoted to admin at boot (existing rows only — never provisions; keep public signup closed while set, anyone can claim an unowned address). Required to reach /config with RBAC_ENABLED=true.
VIEWER_EMAILSunsetComma-separated addresses promoted to the restricted viewer role at boot (same existing-rows-only contract). Viewers read, create and toggle todos; deletes and paid actions deny with 403 under enforcement and render disabled with a message. An address in both lists ends admin. See the RBAC demo in Admin & Dashboard.

Build metadata

VariableDefaultPurpose
BUILD_LABELdevGit tag baked in via -ldflags="-X main.Version=..."; shown on the navbar version badge
BUILD_COMMIT(empty)Short SHA baked in via -ldflags="-X main.CommitHash=..."; shown next to the label

The Makefile sets these automatically. See Deploy for the docker buildx version.

Runtime constants

Not every tunable belongs in config.go. Runtime constants that are implementation details of a single package stay in that package to keep cohesion — config/config.go documents every env var and the most commonly tuned runtime constants.

ConstantLocationDefaultPurpose
DefaultReplayBufferSizeconfig/config.go64Per-client SSE replay ring-buffer length
DefaultClientQueueSizeconfig/config.go64Per-client SSE channel buffer
DefaultSSEHeartbeatIntervalconfig/config.go15sSSE heartbeat to detect disconnection
BuildLabelconfig/config.godevGit tag on the navbar badge
BuildCommitconfig/config.go(empty)Short SHA on the navbar badge
DefaultBaseURL (GoAI)internal/llm/goai.gohttps://api.openai.com/v1OpenAI-compatible base URL
DefaultModel (GoAI)internal/llm/goai.gogpt-4o-miniDefault LLM model
DEFAULT_VOLUMEweb/resources/static/cuelume.js0.4Sound playback volume (cuelume's mixer runs hotter than libraries tuned for 30%)