Skip to main content

Configuration reference

Every Ouroboros service is configured by environment variables, and this page lists every one of them, grouped by the service and feature that reads it. Use it when you set up a deployment, when a service refuses to start and names a variable, or when another page sends you here to look one up.

The list is generated from the repository's .env.example — the template every installation starts from — so it always names exactly the variables the code reads.

Where variables are set​

Each service reads its variables when it starts. Change one and restart that service; nothing picks up a change while it runs.

  • In a container, set them in the container's environment — your compose file's environment: block or your platform's secret store. A real environment variable always wins, so a container runs with exactly what it was started with.
  • On a development checkout, run yarn setup once. It copies .env.example into a .env file beside it (and beside each module's own template), generating a real secret for every placeholder. A module's own .env is read after the root one and wins where both declare a variable.

Never commit a real .env file. The committed .env.example holds development values only: none of them is a credential, and none of them is fit for a deployment.

The rules every variable follows​

  1. Everything Ouroboros-specific starts with OURO_, so a container can inherit unrelated environment without a collision. The two exceptions are BetterAuth's own BETTER_AUTH_URL and BETTER_AUTH_SECRET, which keep the names that library and its documentation use.
  2. Platform standards stay unprefixed. PORT is the port a service listens on, as container platforms set it; NODE_ENV and HOSTNAME are read as the platform means them. Every image already sets PORT, and the UI and REST images set NODE_ENV=production.
  3. Configuration is checked at start-up. A service with a missing or malformed variable exits at once, naming the variable — it never starts half-configured and fails on the first request instead.
  4. Secrets never reach the logs. A service redacts every secret from the configuration it logs.

Reading an entry​

Each variable below has its own entry, which you can link to — for example OURO_VAULT_MASTER_KEY. An entry starts with its Development default:

  • A value, such as 15, is what .env.example sets for development. Where the description says what an unset variable means, that is the value a deployment gets when you leave it out; addresses such as http://localhost:4000 are development values that a deployment always replaces.
  • A secret, generated by yarn setup marks a placeholder that must never be deployed. Generate your own, for example with openssl rand -base64 32, and give every service that shares it the same value.
  • Unset. Example: … is a variable the template leaves out. Set it only when you need what its description offers.

The description explains what the variable does, the values it accepts and what happens when it is wrong.

What can go wrong​

  • A service exits at start-up naming a variable. The variable is missing or its value is malformed. Look it up below, fix it in that service's environment and start the service again.
  • Two services refuse to talk to each other. A secret they share differs between them — OURO_ENGINE_SHARED_SECRET must be identical for the REST service and the engine. Set both from the same value.
  • Sign-in fails at the last step. The addresses do not agree: the GitHub OAuth app's callback, BETTER_AUTH_URL and the UI's public address must name the same origin.
  • Every stored provider key stops working. OURO_VAULT_MASTER_KEY changed or was lost. It cannot be recovered from the database; back it up separately.

Database — ouroboros-db, served by docker compose up (see docker-compose.yml)​

OURO_DB_HOST​

Development default: localhost

Host the database is reachable on. The compose stack is always localhost; this is read by ouroboros-db/run.sh, which can migrate any PostgreSQL, not only that one.

OURO_DB_USER​

Development default: ouroboros

Role that owns the local database. Created by the container on first boot; changing it after that needs docker compose down -v to take effect.

OURO_DB_PASSWORD​

Development default: ouroboros

Password for the role above. Local development only — the compose stack publishes the port on 127.0.0.1, so nothing off this machine can reach it.

OURO_DB_NAME​

Development default: ouroboros

Database name. Created by the container on first boot.

OURO_DB_SCHEMA​

Development default: ouroboros

Schema that Flyway owns and migrates. Created by Flyway if it does not exist.

OURO_DB_PORT​

Development default: 5432

Host port the database is published on. Change it if something already holds 5432; the port inside the compose network is always 5432.

OURO_DATABASE_URL​

Development default: postgresql://ouroboros:ouroboros@localhost:5432/ouroboros

Connection string used by ouroboros-rest and by the ouroboros-db scripts. Keep it consistent with the five variables above — compose does not derive it for you.

Local model host — the optional --profile ollama container​

OURO_OLLAMA_PORT​

Development default: 11434

Host port the Ollama daemon is published on. Read only by docker-compose.yml, and only when that profile is selected; the port inside the compose network is always 11434.

Nothing in ouroboros-rest reads this. An Ollama connection's address is a Host field on a provider card, stored in provider_connections.base_url — one workspace may point at this container and another at a workstation down the hall, which is a row rather than a variable. Change this only if something already holds 11434 on your machine, and then type the port you chose into the card.

Service wiring — the URLs each module uses to reach the next one​

OURO_REST_URL​

Development default: http://localhost:4000

Base URL of ouroboros-rest, read by ouroboros-ui. The UI reaches nothing else. ouroboros-rest reads it too, for one thing: the GitHub OAuth redirect_uri, which has to be the URL registered against the OAuth app rather than whatever Host header a request arrived with. An origin — scheme, host and optional port, no path, no trailing slash. ouroboros-engine's simulated-run driver reads it as where to report the runs it scripts.

OURO_UI_URL​

Development default: http://localhost:3000

Where a browser lands after signing in or out. ouroboros-ui's own origin: the OAuth callback is a redirect a person follows rather than a fetch a script made, so it has to end somewhere with a page on it, and ouroboros-rest serves none.

OURO_ENGINE_URL​

Development default: http://localhost:8000

Base URL of ouroboros-engine, read by ouroboros-rest. The engine is internal and is never reachable from a browser.

OURO_ENGINE_SHARED_SECRET​

Development default: a secret, generated by yarn setup.

Shared secret for the internal REST-to-engine call, sent as X-Ouro-Internal-Key and compared in constant time. Both sides must carry the same value.

OURO_RUN_SIMULATOR_SECRET​

Development default: a secret, generated by yarn setup.

The second value X-Ouro-Internal-Key may carry, and the one that makes a run simulated. A run's simulated watermark follows the principal that opened it, and the only thing a caller proves on the internal channel is which secret it holds — so the simulated-run driver presents this one and a real executor presents OURO_ENGINE_SHARED_SECRET.

ouroboros-engine reads it too, in development: set, an engine that carries the driver (never the production image) mounts its /dev routes, and uv run python -m ouroboros_simulator presents it.

Optional. Unset means this deployment runs no simulator and every run opened through the ingestion contract is a real one. It may not equal OURO_ENGINE_SHARED_SECRET: two principals presenting one proof are one principal, and ouroboros-rest refuses to boot.

Generate a strong value, e.g.: openssl rand -base64 32

Authentication — ouroboros-rest​

BETTER_AUTH_SECRET​

Development default: a secret, generated by yarn setup.

What BetterAuth signs sessions and encrypts stored OAuth tokens with; at least 16 characters. Rotating it signs everybody out and makes stored tokens undecryptable.

Generate a strong value, e.g.: openssl rand -base64 32

BETTER_AUTH_URL​

Development default: http://localhost:4000

The origin BetterAuth builds its own URLs from — its sign-in redirect and its OAuth callback. ouroboros-rest's public address, so the same value as OURO_REST_URL above; nothing derives one from the other, so keep them in step. An origin: scheme, host and optional port, with no path — BetterAuth adds its own /api/auth.

OURO_GITHUB_CLIENT_ID​

Development default: dev-github-client-id

GitHub OAuth application used for sign-in — BetterAuth's provider reads both. Register a development app under Settings -> Developer settings -> OAuth Apps with the authorization callback URL http://localhost:4000/api/auth/callback/github and paste its credentials here. That URL is ${BETTER_AUTH_URL}/api/auth/callback/github: the library builds it from BETTER_AUTH_URL rather than from OURO_REST_URL, and GitHub compares what was registered against what the exchange presents, so a difference of one character is a sign-in that fails at the last hop.

OURO_GITHUB_CLIENT_SECRET​

Development default: dev-github-client-secret

Described with OURO_GITHUB_CLIENT_ID above.

OURO_GITHUB_API_BASE_URL​

Development default: https://api.github.com

Where GitHub's REST API is. The public API, written out rather than left to the client's own default so that the template and the code state the same address.

Change it for a GitHub Enterprise Server installation — the address its API answers on, including the path:

OURO_GITHUB_API_BASE_URL=https://ghe.example.com/api/v3

It moves the address and nothing else. The credential is still the workspace's or the source's own token, the rate-limit rules are unchanged, and every failure is classified by the same code. The one other thing that sets it is the e2e suite's compose override, which points it at the sandbox tracker a planning push may create issues in — see tests/e2e/README.md.

Credential vault — ouroboros-rest​

OURO_VAULT_MASTER_KEY​

Development default: b3Vyb2Jvcm9zLWRldi12YXVsdC1tYXN0ZXIta2V5ISE=

The key-encryption key the credential vault seals every workspace's data-encryption key with. Exactly 32 bytes, base64:

openssl rand -base64 32

Exactly, not at least. A value of nearly the right length is one that was edited by hand or truncated in transit, and a service that stretched it to fit would seal credentials with bytes derived from a mistake. ouroboros-rest validates it at boot and exits non-zero naming this variable, printing no part of the value.

What it protects: this key encrypts the ouroboros.tenant_keys table and nothing else. Every provider credential is encrypted with a per-workspace key sealed there, which is why moving custody to AWS KMS or Vault/OpenBao later re-wraps that one table and leaves every credential ciphertext untouched.

LOSING THIS VALUE LOSES EVERY STORED CREDENTIAL, and there is no recovery path — that is what per-tenant sealing means. In the default deployment its custody is the operator's problem, stated plainly rather than as "KMS-backed"; docs/SECURITY_MODEL.md is where the whole model is written down. Rotating it is a re-wrap of tenant_keys and rewrites no credential.

OURO_CORS_ORIGINS​

Development default: http://localhost:3000

Browser origins allowed to call ouroboros-rest with credentials — the origins the session cookie is permitted to travel to. Comma-separated, and each entry is an origin rather than a URL: a scheme, a host and an optional port, with no trailing slash and no path. A wildcard is not accepted, because a credentialed cross-origin request may not be answered with one. The development value is ouroboros-ui, which is the only browser origin there is.

OURO_DASHBOARD_POLL_SECONDS​

Development default: 15

Seconds a dashboard client is told to wait between polls — the value ouroboros-rest sends as X-Ouro-Poll-After on every dashboard answer, 200 and 304 alike. The UI treats the latest value as its effective interval, so raising this is how a deployment under load slows every open dashboard within one poll cycle, with nothing shipped to the client. Whole seconds, 1–3600; 15 is the interval the contract documents for a visible tab.

OURO_LISTEN_HOST​

Development default: 127.0.0.1

Which interface ouroboros-rest binds, when a stack has to choose it explicitly. Accepted values are exactly 127.0.0.1 and 0.0.0.0; anything else fails validation at boot. LEAVE IT AT THE VALUE BELOW: unset (or 127.0.0.1, which is the same posture in development) is what every deployment should inherit — NODE_ENV alone decides the interface, loopback outside production, every interface in it. The one stack that sets it to 0.0.0.0 is the e2e override, where the containerised service runs non-production so the seeded password sign-in answers, and loopback would be an interface Docker's port publishing cannot route to. It moves the interface only; the sign-in gate stays NODE_ENV's and gains no second switch.

Build-farm runner identity and the agent gateway — the forwarded client certificate, the version floor​

OURO_FARM_CLIENT_CERT_HEADER​

Development default: unset. Example: x-ouro-client-cert

The request header a trusted reverse proxy forwards a runner's TLS client certificate in. Unset by default, and that is the posture every deployment that terminates TLS inside ouroboros-rest should stay in: with it unset the certificate is read from the TLS socket and from nowhere else.

Set it only if something in front of this service terminates TLS. A proxy that does so has already consumed the client certificate, and mTLS becomes decoration unless the proxy passes it through — docs/SECURITY_MODEL.md's farm-CA section is where that requirement is written down, with the nginx and Traefik directives that satisfy it.

WHY IT IS OFF BY DEFAULT: a certificate is public. It crosses the network in the clear at every handshake, so anybody can obtain a copy of one — and a header this service trusts unconditionally is a header anybody can send. Naming one here is an assertion by the operator that this header cannot reach the process except through their proxy.

OURO_FARM_MIN_AGENT_VERSION​

Development default: 0.0.0-0

The oldest ouroboros-runner build the agent gateway accepts, as a semantic version. Unset by default: no agent-version floor.

The agent runs on customer machines and cannot be force-upgraded, so the right to refuse an old one has to exist from the first release. An agent whose hello reports a version below this is answered refuse {version.below_minimum} with a sentence naming the floor, and exits rather than reconnecting into the refusal — so raise it only once the fleet has a newer build to move to. The protocol-line floor is separate and is the build's own.

The template's value is 0.0.0-0, the lowest version SemVer can express: every build — including an unstamped development build, which reports 0.0.0-dev — is at or above it, so a copied template declares the floor without setting one. A fleet floor looks like 0.2.0. Unset means the same as this value.

OURO_FARM_PUBLIC_URL​

Development default: https://localhost:8443

The https origin runner machines reach this deployment at. ouroboros-rest serves the runner installer itself — GET /install.sh — and writes this into it, so the build farm's one-liner downloads the agent from this deployment and enrols it into this deployment, with no public host between a build machine and the binary it runs. An origin: https, a host and an optional port, no path. Unset, the installer uses OURO_REST_URL when that is https, and declines with a message naming this variable when it is not. Locally it is the TLS-terminating proxy OURO_RUNNER_SERVER below names, because the agent speaks TLS only and ouroboros-rest serves plain HTTP on 4000.

OURO_FARM_RELEASES_DIR​

Development default: ../ouroboros-runner/dist

The directory ouroboros-rest serves runner releases from: one subdirectory per version, each exactly as ouroboros-runner's make release writes dist/<version>/ and the GitHub release ouroboros-runner-v<version> carries it — three binaries, install.sh and SHA256SUMS. A deployment copies a release in; it is served at once, without a restart. Unset, the deployment serves no installer. A relative path is resolved against ouroboros-rest's working directory, so this value is make release's own output when REST runs from its module directory, as yarn dev does.

OURO_FARM_LOG_BUDGET_BYTES​

Development default: 2147483648

The bytes of finished builds' logs one workspace keeps. Build logs live in PostgreSQL, so the retention sweep holds each workspace under this budget by removing the oldest finished jobs' logs — whole, never in part — as well as every log older than thirty days. A running build's log is never swept. A whole number of bytes between 1048576 (1 MiB) and 1099511627776 (1 TiB); unset means 2147483648 (2 GiB), the value written here.

Build-farm artifact store — ouroboros-rest​

OURO_ARTIFACT_STORE​

Development default: local

Where a build job's uploaded artifacts are kept: local — a directory ouroboros-rest mounts, the default — or s3, any S3-compatible store (S3 or MinIO). A local volume does not scale horizontally: two replicas do not share a disk, and a deployment that needs to switches to s3 by configuration alone. Each artifact row records the driver it was written with, so the two coexist while old rows are moved.

OURO_ARTIFACT_DIR​

Development default: .artifacts

The local store's directory. A relative path is resolved against ouroboros-rest's working directory; .artifacts there is gitignored. A deployment mounts a volume and names it here. Read only when OURO_ARTIFACT_STORE is local.

OURO_ARTIFACT_S3_ENDPOINT​

Development default: http://localhost:9000

The S3-compatible endpoint, as an http(s) origin with no path — the bucket is added as a path segment (path-style addressing, which MinIO requires and S3 serves). Read only when OURO_ARTIFACT_STORE is s3, when this and the three below are required. The values here are docker-compose.yml's minio profile's.

OURO_ARTIFACT_S3_BUCKET​

Development default: ouroboros-artifacts

The bucket artifacts are written to. It must already exist: a store that created buckets on demand would turn a typo into a new, empty bucket.

OURO_ARTIFACT_S3_REGION​

Development default: us-east-1

The region requests are signed for. MinIO accepts any; unset means us-east-1.

OURO_ARTIFACT_S3_ACCESS_KEY_ID​

Development default: ouroboros

The S3 access key id.

OURO_ARTIFACT_S3_SECRET_ACCESS_KEY​

Development default: ouroboros-dev-minio-secret

The S3 secret access key — redacted from every log line. The value here is the compose MinIO's development password, not a credential.

OURO_ARTIFACT_QUOTA_BYTES​

Development default: 10737418240

The bytes of live artifacts one workspace keeps. A file that would cross it is not stored and the job carries an artifact_quota_exceeded warning — never a failed build. A whole number of bytes between 1024 and 1125899906842624 (1 PiB); unset means 10737418240 (10 GiB).

OURO_ARTIFACT_MAX_FILE_BYTES​

Development default: 67108864

The per-file cap every job offer carries. A larger file is uploaded cut to it and listed as truncated. Between 1024 and 68719476736 (64 GiB); unset means 67108864 (64 MiB).

OURO_ARTIFACT_MAX_JOB_BYTES​

Development default: 268435456

The per-job cap every job offer carries; files past it are listed as skipped. At least the per-file cap, and at most 68719476736; unset means 268435456 (256 MiB).

OURO_ARTIFACT_RETENTION_DAYS​

Development default: 30

How many days an uploaded artifact is kept in a workspace that has not set its own artifacts retention tier — mockup 11's retained 30d. Between 1 and 3650; unset means 30.

Build-farm runner agent — ouroboros-runner​

OURO_RUNNER_SERVER​

Development default: https://localhost:8443

The control plane the agent enrols with and connects to, https:// only: the first request carries a token and every later one is authenticated by a client certificate. NOTE that the agent reads NO .env file — it runs on a customer's machine, configured by its service unit — so these six are fallbacks for the ouroboros-runner flags of the same meaning, listed here because this file is the complete list. Locally, ouroboros-rest serves plain HTTP on 4000, so running the agent against it needs a TLS-terminating proxy in front (the nginx and Traefik directives are in docs/SECURITY_MODEL.md § 7.6); this is where such a proxy listens.

OURO_RUNNER_TOKEN​

Development default: orb_enroll_placeholder-mint-one-per-machine

The enrollment token, for ouroboros-runner enroll (--token). The variable exists to keep a token off the process list. It is minted by an owner or admin (POST /api/v1/farm/enrollment-tokens), spent on first use, and never written anywhere by the agent. The value here is a placeholder, not a token.

OURO_RUNNER_STATE_DIR​

Development default: /var/lib/ouroboros-runner

Where the agent keeps its identity (--state-dir): the client certificate and key, the farm CA it pins, and its unacknowledged terminal frames — a 0700 directory of 0600 files.

OURO_RUNNER_SERVER_CA​

Development default: /etc/ouroboros-runner/server-ca.pem

PEM roots to verify the control plane with, in place of the system's (--server-ca). Needed for a private deployment, and for the development proxy above, whose certificate no system trusts. Not the farm CA: that signs runner certificates, and the agent pins it by itself.

OURO_RUNNER_NO_SHELL​

Development default: false

true: run no job directly on this machine. The hello then reports shell: false and every shell offer is declined, while container jobs still run — the refusal a machine holding signing keys makes. Shell jobs run with no sandbox, as the agent's user: the tenant's machine running the tenant's command (ouroboros-runner/README.md).

OURO_RUNNER_KEEP_WORKSPACE_ON_FAILURE​

Development default: false

true: leave the workspace of a job that failed, timed out or errored under <state-dir>/work/<job id> for diagnosis. By default every workspace is removed when its job ends.

OURO_RUNNER_LOG_CAP_BYTES​

Development default: 67108864

The most of one job's output the agent sends, 65536–268435456 — the same range as the control plane's own per-job cap. Output past it is reported as dropped, which the run console renders as an elision rather than as a log that looks complete. An agent that shipped four gigabytes for the control plane to discard would already have done the damage.

Local model providers — ouroboros-rest's engine-facing surface​

OURO_LOCAL_PROVIDER_URLS​

Development default: ollama=http://localhost:11434

Where this deployment's local model providers are, as comma-separated kind=url pairs:

OURO_LOCAL_PROVIDER_URLS=ollama=http://localhost:11434,openai_compatible=http://localhost:8001/v1

Unset by default, and that is the normal posture: most installations run no local model server, and a lease for one then answers 404 rather than pretending something is listening on a port nobody mentioned.

What it is for. The architecture says workers never hold provider credentials: ouroboros-engine asks ouroboros-rest to make every cloud model call and the key never leaves the control plane. Local providers are the one exception, because there is no key on that path to protect — an engine worker calling an Ollama daemon on the same box gains nothing from proxying its traffic — so it asks POST /internal/credentials/lease and is told an address. Only an address: a lease has no field a credential could arrive in.

Only ollama and openai_compatible may appear here. Naming anthropic, copilot or cursor stops the process at boot rather than being ignored, because an operator who wrote it believes their workers reach that provider directly, and a service that started anyway would leave them believing it. Each address is an absolute http:// or https:// URL and keeps its path — vLLM is served at /v1, and an address truncated to its origin sends every request to a 404 that reads like the model server being down.

From inside the compose network, "localhost" is the container rather than the machine: a daemon running on the host is reachable at host.docker.internal, and one running as a service in the stack is reachable by its service name.

A later release replaces this with provider_connections rows; until it lands, the operator's declaration is the only thing that can say where a local provider is.

The value below is Ollama's own default address, which is what a developer running it locally already has. A checkout without Ollama is not broken by it: a lease is an address rather than a health check, so the surface answers with this URL and the call that follows fails against nothing listening — which is the same thing that happens to any other local service that is not up. Delete the line to declare no local providers at all.

Provider health — the routing page's honest status strip​

OURO_PROVIDER_HEALTH_INTERVAL_SECONDS​

Development default: 60

How often ouroboros-rest checks whether a workspace's model providers are reachable, in seconds. Optional; 60 when unset, which is what the strip's promise is written against — a stopped Ollama daemon shows on the page within one cycle.

The delay is jittered by +/-25% around this value, first cycle included, so a fleet of self-hosted instances restarted together does not converge on one schedule and arrive at a provider's endpoint in the same second forever. This is the nominal interval, not the actual one.

It is also the age at which a local provider's last check counts as stale. Between 10 and 86400. There is deliberately no value that turns the sweep off: a strip that has stopped updating and a strip that honestly says "unknown" look different to a person, and only one of them is true.

What the sweep actually does is bounded by design: Ollama is asked for its tag listing, an OpenAI-compatible server for its model listing, and Anthropic for a models-list key validation. No completion request is ever issued — nothing here costs a workspace money, and nothing here is billed per call.

OURO_PROVIDER_HEALTH_KEY_CHECK_SECONDS​

Development default: 900

How old a cloud provider's key validation may get before it is redone, in seconds. Optional; 900 (fifteen minutes) when unset. Between 60 and 86400.

Much slower than the sweep above, and separate from it, because the two ask different people. A local daemon is the operator's own machine. A vendor's key-validation endpoint is somebody else's rate-limited service being asked by every self-hosted Ouroboros in the world, and what the check detects — a rotated or revoked key — happens on a human timescale. The sweep still runs on the shorter interval; this is how old a cloud row's last check has to be before that sweep touches it.

OURO_BACKLOG_SYNC_INTERVAL_SECONDS​

Development default: 300

How often the backlog sync polls GitHub, in seconds. Optional; 300 (five minutes) when unset. Between 60 and 86400.

This is what the intake page's "synced 40s ago" tag counts from. A cycle asks every enabled repository for the issues updated since its stored watermark, so an incremental poll costs roughly one request per repository — a workspace watching fifty of them spends about six hundred of a token's five thousand hourly requests at this cadence.

The delay is jittered by +/-25% around this value, first cycle included, the same rule the health sweep above follows and for the same reason. A cycle that stopped at its per-poll cap — a cold import of a large backlog — books the next one a second later instead of waiting a full interval, so a first sync is several quick cycles rather than an afternoon.

There is deliberately no value that turns polling off. A backlog that has quietly stopped updating and one whose freshness tag says how old it is look different to a person, and only one of them is honest; an operator who wants the backlog now has the manual re-sync.

OURO_ESTIMATION_CONCURRENCY​

Development default: 4

How many issues the estimation pipeline sizes at once. Optional; 4 when unset. Between 1 and 32.

The bound on outbound calls to ouroboros-engine for sizing, and on the pool connections their writes hold. It is what keeps a cold import of a large backlog from opening one engine connection per issue: the work queue admits this many at a time and the rest wait their turn in memory.

Four keeps a single-process engine busy over a network hop without overwhelming it. Raising it much past the database pool's ten connections stops buying anything, because the extra work then queues on the pool instead.

OURO_ESTIMATION_CONFIDENCE_FLOOR​

Development default: 70

Below what confidence an estimate sends its issue to needs human, as a percentage. Optional; 70 when unset. Between 0 and 100.

The transition is this service's policy, not the estimator's: ouroboros-engine reports how sure it is and has no needs_human field at all. 70 is the number the bundled heuristic's tables were calibrated against, restated here so an installation can disagree with it without a release — 0 accepts every estimate, 100 sends every issue to a person.

An estimate below the floor is still stored in full, trace and all. It is a real answer beside a needs human pill, not a failure.

OURO_ESTIMATION_STALE_SECONDS​

Development default: 600

How long an issue may sit in estimating before the sweep re-queues it, in seconds. Optional; 600 (ten minutes) when unset. Between 60 and 86400.

Not a timeout on an estimate — the engine gateway's own five-second deadline is that — but how long a row may claim to be estimating with nothing estimating it. A process killed mid-flight is the case: the row says estimating, no queue holds it, and without this sweep the page would show that pill until somebody re-estimated by hand.

OURO_ESTIMATION_SWEEP_INTERVAL_SECONDS​

Development default: 120

How often that sweep runs, in seconds. Optional; 120 (two minutes) when unset. Between 10 and 86400.

Faster than the backlog sync because it knocks on a different door: one indexed query against this deployment's own database, which usually returns nothing. A row stranded by a restart comes back within this plus OURO_ESTIMATION_STALE_SECONDS. Jittered by +/-25%, like every other loop here.

Run controls — ouroboros-rest​

OURO_RUN_CONTROL_TTL_SECONDS​

Development default: 120

How long a pause, resume or abort is worth delivering, in seconds. Optional; 120 (two minutes) when unset. Between 10 and 3600.

A control is a row on a durable queue, and one the executor has not answered by then is marked expired, which the console renders as "no response — the run may be between stages" rather than as success.

OURO_RUN_STEER_TTL_SECONDS​

Development default: 300

How long a steer is worth delivering, in seconds. Optional; 300 (five minutes) when unset. Between 10 and 3600. Longer than the buttons', because a steer lands at the executor's next point of context injection and a nudge that arrives a little late is still the nudge.

OURO_RUN_CONTROL_SWEEP_SECONDS​

Development default: 15

How often the control-expiry sweep runs, in seconds. Optional; 15 when unset. Between 5 and 3600. One indexed update against this deployment's own database; the console's listing and the executor fetch sweep their own run first, so this only decides how soon an expiry reaches the audit trail for a run nobody is watching. Jittered by +/-25%.

Workspace lifecycle — ouroboros-rest​

OURO_LIFECYCLE_PURGE_SWEEP_SECONDS​

Development default: 3600

How often the workspace-purge sweep runs, in seconds. Optional; 3600 when unset. Between 60 and 86400. A deleted workspace is recoverable for 30 days; the sweep purges each one whose window has closed — its rows, its artifact objects and its data-encryption key. Jittered by +/-25%.

Outbound webhooks & SIEM streaming — ouroboros-rest​

OURO_WEBHOOK_DISPATCH_SECONDS​

Development default: 5

How often the webhook dispatcher runs, in seconds. Optional; 5 when unset. Between 1 and 300. Each tick queues a delivery for every endpoint subscribed to a new event and sends every attempt that is due, so this is the delay between an event and its first attempt. Jittered by +/-25%.

OURO_WEBHOOK_MAX_ATTEMPTS​

Development default: 5

How many attempts a delivery gets before it lands in the dead-letter queue. Optional; 5 when unset. Between 1 and 20. Retries back off exponentially (30s, 1m, 2m, 4m … capped at 1h).

OURO_WEBHOOK_INTERNAL_ALLOWLIST​

Development default: unset. Example: collector.internal,10.20.0.0/16

Internal collectors a webhook may reach despite the SSRF policy, comma-separated: hostnames, IP addresses or CIDR blocks (collector.internal,10.20.0.0/16). Optional; empty means none. By default a webhook may not reach loopback, link-local (including the cloud metadata address 169.254.169.254), RFC1918, carrier-grade NAT or unique-local addresses — at save and again at every delivery. This never relaxes https. Left commented: unset means no internal target is reachable, which is the only safe default for a template that may be copied anywhere.

Backlog health & nightly re-estimation — ouroboros-rest​

OURO_BACKLOG_STALE_DAYS​

Development default: 30

How many days without a tracker update make an open ticket stale on the planning page's Backlog Health card. Optional; 30 when unset. Between 1 and 3650.

OURO_REESTIMATION_HOUR_UTC​

Development default: 2

The UTC hour the nightly re-estimation job runs at — it queues open, unsized canonical tickets through the estimation pipeline. Optional; 2 when unset. Between 0 and 23.

OURO_REESTIMATION_JITTER_MINUTES​

Development default: 30

How many minutes after that hour a night's run may land, chosen at random each night so a fleet of installations does not reach for its engines in the same second. Optional; 30 when unset. Between 1 and 180.

OURO_REESTIMATION_BATCH​

Development default: 100

The most unsized tickets one night's run queues, across every workspace and shared out between them. What the bound leaves behind is picked up the next night. Optional; 100 when unset. Between 1 and 1000.

Flake scorer — ouroboros-rest​

OURO_FLAKE_RESCORE_HOUR_UTC​

Development default: 3

The UTC hour the nightly flake re-scorer runs at — it re-scores each workspace's active flake cases under the latest formula and records a bookkeeping row per workspace. Each night's pass lands at a random minute in the hour after it. Optional; 3 when unset. Between 0 and 23.

OURO_FLAKE_RESCORE_CAP​

Development default: 2000

The most cases one workspace's nightly pass re-scores, least recently scored first; what the bound leaves is first in line the next night. Optional; 2000 when unset. Between 1 and 100000.

Knowledge — the fact staleness sweep — ouroboros-rest​

OURO_FACT_SWEEP_HOUR_UTC​

Development default: 4

The UTC hour the nightly fact staleness sweep runs at — it matches every merged PR of an enabled repository since each anchor was last checked against confirmed facts' anchors, and flags the facts they touch stale for review. A merge the source sync observes is swept at once as well. Each night's pass lands at a random minute in the hour after it. Optional; 4 when unset. Between 0 and 23.

Knowledge — the repo-map generator — ouroboros-rest​

OURO_REPO_MAP_HOUR_UTC​

Development default: 5

The UTC hour the nightly repo-map generator runs at — for every enabled repository it lists the tree once, reads CODEOWNERS and the newest detection scan, and publishes a new generated repo-map skill version only when the map changed. Each night's pass lands at a random minute in the hour after it. Optional; 5 when unset. Between 0 and 23.

Insights rollups — ouroboros-rest​

OURO_INSIGHTS_ROLLUP_INTERVAL_SECONDS​

Development default: 3600

How often the Insights rollup ticks, in seconds. Optional; 3600 (an hour) when unset. Between 60 and 86400. Each tick re-fills today's daily rows for every workspace and metric family, steps any backfill, and — on the first tick of a new UTC day — consolidates the trailing window below. Jittered by +/-25%, like every other loop here.

OURO_INSIGHTS_ROLLUP_CONSOLIDATE_DAYS​

Development default: 3

How many days, ending yesterday, the nightly consolidation re-fills. Optional; 3 when unset. Between 1 and 31. Late data — a host sync that ran late, a revert that landed days after the merge it undoes — reaches the day it belongs to within this window.

OURO_INSIGHTS_ROLLUP_BACKFILL_DAYS​

Development default: 90

How far back, in days, a metric family's first fill reaches. Optional; 90 (the page's longest range) when unset. Between 1 and 730. Also the furthest a consolidation after an outage reaches.

OURO_INSIGHTS_ROLLUP_DAYS_PER_TICK​

Development default: 31

The most backfill days one tick fills per family. Optional; 31 when unset. Between 1 and 366. What a tick leaves, the next resumes at the cursor — an interrupted backfill neither skips nor double-counts a day.

Mail and the weekly Insights digest — ouroboros-rest​

OURO_SMTP_URL​

Development default: smtp://localhost:1025

The SMTP server ouroboros-rest sends mail through: smtp://host:port, or smtps://user:password@host:port for a relay that wants TLS and a login. Optional. Unset, this deployment sends no mail: the weekly Insights digest is off, the API says so (mail.transport: "none"), and subscribing is refused rather than accepted into silence.

The development value is mailpit, which catches every message and shows it at http://localhost:8025 — start it with docker compose --profile mail up -d mailpit. Nothing leaves the machine. It may carry a password, so boot logs print it with the password masked.

OURO_MAIL_FROM​

Development default: no-reply@ouroboros.localhost

The address mail is sent from — a bare address, no display name; mail goes out as "Ouroboros". Required when OURO_SMTP_URL is set, and refused at boot without it.

OURO_INSIGHTS_DIGEST_INTERVAL_SECONDS​

Development default: 300

How often ouroboros-rest checks whether a workspace's weekly digest has come due, in seconds. Optional; 300 (five minutes) when unset. Between 5 and 3600. This is how late after its slot a digest may leave, not how often one is sent: each workspace's slot is a day of the week and a UTC time its administrators choose (Monday 09:00 until they do). Jittered by +/-25%.

Onboarding — the Get Started wizard's template tiles and smart defaults​

OURO_ONBOARDING_UNLOCK_THRESHOLD​

Development default: unset. Example: 10

The merged-loop count that unlocks an advanced onboarding template, replacing each template's own rule — the Deep refactor tile's "unlock after 10 merged loops". Optional; unset, every template's shipped rule stands. A whole number between 0 and 10000; 0 unlocks every tier.

OURO_MANAGED_KEY_POOL​

Development default: false

Which pools this deployment runs for its workspaces — what the wizard's Smart Defaults card is allowed to promise. Both are false unless declared, which is the self-hosted default: the card then says "bring your own keys" and "enroll a runner" instead of offering managed keys or a hosted runner that do not exist here. true or false; the pools themselves arrive in a later release.

OURO_HOSTED_RUNNER_POOL​

Development default: false

Described with OURO_MANAGED_KEY_POOL above.

OURO_MANAGED_KEY_TRIAL_CENTS​

Development default: unset. Example: 500

The trial credit the managed key pool gives a new workspace, in whole cents — 500 prints "$5 trial credit". Optional; unset, the managed row names no figure. Between 1 and 1000000, and refused at boot unless OURO_MANAGED_KEY_POOL is true.

OURO_DATA_REGION​

Development default: unset. Example: eu-central-1

Where this deployment keeps its data, as you would name it — shown read-only on the Settings workspace card. Optional; unset, the card says "self-hosted". A label without spaces: letters, digits and . _ : / ( ) -, up to 64 characters. It describes where you deployed and moves nothing.

Observability​

OURO_LOG_LEVEL​

Development default: info

Log verbosity for ouroboros-engine: debug, info, warning or error.

Testing — ouroboros-rest's integration harness​

OURO_TEST_DATABASE_DISPOSABLE​

Development default: false

Whether yarn test:integration may empty the database between tests.

The harness normally starts a throwaway PostgreSQL of its own, migrates it, and empties it between tests; a container it started is disposable by definition and this variable is not consulted. It matters only when OURO_DATABASE_URL is exported to point the suite at a database somebody else started — the compose stack above, most likely — because emptying that one takes the development seed with it, and no migration puts it back.

So it stays false here, and the harness refuses to truncate. Set it to exactly true for a run against a database you are genuinely willing to lose. Nothing outside the test suites reads it.

Documentation screenshots — ouroboros-docs's capture harness​

OURO_DOCS_CAPTURE_BASE_URL​

Development default: http://localhost:3000

The seeded ouroboros-ui that yarn screenshots (in ouroboros-docs) signs in to and photographs. It signs in with the development seed's email/password credential, which ouroboros-rest only accepts outside production — so this is always a development or e2e stack, never a deployment. Nothing in the application reads it.