> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tessary.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration reference

> Look up every environment variable used to configure a self-hosted Tessary deployment, grouped by purpose.

## Source of truth

`.env.example` at the repository root is the authoritative list of environment variables for a self-hosted deployment. You do not need it to start Tessary: every variable has a working default, and `docker compose up -d` boots on a clone with no `.env` at all. Copy it when you want to change something:

```bash theme={null}
cp .env.example .env
```

The sections below group the variables most deployments need to set or understand.

<Note>
  This page does not restate every comment in `.env.example`. For a variable not covered below, read its comment in the file itself rather than assuming a default.
</Note>

## Database

Compose builds the Postgres connection from these values. You only set `TESSARY_JDBC_URL` directly if you run the backend outside Compose, against a Postgres you manage yourself.

| Variable            | Description                                                                                                                                                                                                        | Default                                          |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |
| `POSTGRES_DB`       | Name of the Postgres database.                                                                                                                                                                                     | `tessary`                                        |
| `POSTGRES_USER`     | Postgres user Tessary connects as.                                                                                                                                                                                 | `tessary`                                        |
| `POSTGRES_PASSWORD` | Password for `POSTGRES_USER`. The database publishes no host port, so it is reachable only from the Compose network; set your own if you publish that port or share the host.                                      | `tessary`                                        |
| `POSTGRES_DATA_DIR` | What backs the Postgres data volume. A bare name is a Docker named volume; a path (`/data/postgres`) is a host bind. The default keeps the database inside Docker, so a fresh install leaves nothing else on disk. | a named volume (`tessary_tessary-postgres-data`) |

## Secrets

| Variable             | Description                                                                                               | Default                                          |
| -------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `TESSARY_SECRET_KEY` | AES-256-GCM key Tessary uses to seal secrets it stores, such as source credentials and provider API keys. | A placeholder, published in `docker-compose.yml` |

Generate a key with:

```bash theme={null}
openssl rand -base64 32
```

<Warning>
  The shipped default is not a secret. It is written in `docker-compose.yml` in the public repository so a first boot needs no configuration, which means anyone who reaches the database of an instance still using it can decrypt every credential in there. Replace it before the instance holds anything real: see [Secure the instance](/self-hosting/setup#secure-the-instance). Tessary warns on every boot that runs the default, and refuses to start once `SITE_DOMAIN` is set.
</Warning>

Changing this key makes everything already sealed with the old one unreadable, so pick a value per environment and keep it. That is also what a restore depends on: a `pg_dump` carries the ciphertext and not the key, so the target host needs the same `TESSARY_SECRET_KEY` to read the credentials back.

## Auth

| Variable                       | Description                                                                     | Default                                                                     |
| ------------------------------ | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `TESSARY_AUTH_COOKIE_PASSWORD` | Key for the session cookie. Used regardless of which identity provider you use. | A placeholder, published in `docker-compose.yml`                            |
| `TESSARY_AUTH_FRONTEND_URL`    | Where Tessary sends users after sign-in and sign-out.                           | Derived from `HTTP_PORT`, so it follows the port Compose actually publishes |
| `WORKOS_API_KEY`               | WorkOS API key, for bring-your-own SSO.                                         | Optional, ships blank                                                       |
| `WORKOS_CLIENT_ID`             | WorkOS client ID, for bring-your-own SSO.                                       | Optional, ships blank                                                       |

<Warning>
  `TESSARY_AUTH_COOKIE_PASSWORD` is the more urgent of the two placeholders. The sealing key above needs database access to exploit; this one does not. Anyone can forge a session cookie against an instance still running the published default. Replace it before anything but your own machine can reach the instance: see [Secure the instance](/self-hosting/setup#secure-the-instance).
</Warning>

<Note>
  The default identity provider is local email and password; it needs none of the WorkOS variables. Set `WORKOS_API_KEY` and `WORKOS_CLIENT_ID` only if you want to bring your own WorkOS organization for SSO instead.
</Note>

## Sign-up policy

Who may create an account is a setting, not an environment variable: an owner or admin sets it under **Settings → Members → Sign-up policy**, and it is enforced by the backend on every path that creates an account, including a first sign-in through WorkOS. It is stored on the organization, which in the open edition is the whole instance.

| Mode                            | Who gets an account                                                                      |
| ------------------------------- | ---------------------------------------------------------------------------------------- |
| Anyone (`open`)                 | Anyone who can reach the instance. The default, on a fresh install and after an upgrade. |
| Listed email domains (`domain`) | Addresses at the domains you list, such as `acme.com`. Exact domains, not patterns.      |
| Invitation only (`invite`)      | Nobody without a pending invitation from Settings → Members.                             |

In every mode a pending invitation admits its address, so a contractor on a personal address can be let in under `domain`, and `invite` is the invitation list itself. Admission and membership are different things: someone the policy admits creates an account with an organization of their own, while an invitation from Settings → Members both admits them and joins them to yours. The policy gates account creation only: tightening it never removes a member or blocks an existing member's next sign-in, and while the instance has no account at all any mode admits the first one. A refused sign-up creates nothing and answers `403 AUTH.SIGNUP_REFUSED`; the WorkOS callback sends the visitor back to the sign-in screen with that reason. Each change is written to the audit log, and the setting is refused through the raw organization settings endpoint so it can only change through the validated route.

The same setting is readable and writable over the API, for an owner or admin: `GET` and `PUT /api/orgs/{orgSlug}/signup-policy` with a body of `{"mode": "domain", "domains": ["acme.com"]}`. If you are putting the instance on a real hostname, set this before anyone else can reach it.

## Ingest buffer

Between the OTLP endpoint and the database sits a buffer. The default needs nothing beyond the stack you already run; the alternative is a broker, for installs that want an accepted batch to survive a restart.

| Variable                                        | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Default         |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------- |
| `TESSARY_INGEST_SPOOL_MODE`                     | `memory`: a 200 means the batch is queued in the backend process, the buffer sheds with `503 + Retry-After` when its byte budget is full, and a restart loses what was queued (stock exporters retry a 503, so a shed costs latency, not data). `kafka`: a 200 means the batch is persisted by a Kafka-API broker before the response, and nothing acknowledged is lost on restart.                                                                                                        | `memory`        |
| `TESSARY_INGEST_SPOOL_KAFKA_BOOTSTRAP_SERVERS`  | The broker, in `kafka` mode. The bundled single-node Redpanda answers at the default.                                                                                                                                                                                                                                                                                                                                                                                                      | `redpanda:9092` |
| `TESSARY_INGEST_SPOOL_KAFKA_REPLICATION_FACTOR` | Copies the broker keeps of each accepted batch, applied when the spool creates its topics. The bundled single-node Redpanda can only satisfy `1`. Against your own multi-broker cluster this is what decides whether `kafka` mode's promise survives a broker failure: at `1`, an acknowledged batch is on one machine's disk, and losing that machine loses data the API already answered `200` to — set `3` there. The broker is told to wait for two copies whenever this is above `1`. | `1`             |
| `TESSARY_INGEST_SPOOL_KAFKA_CONSUMERS`          | Drainer threads in `kafka` mode, each with its own consumer; the broker spreads the eight partitions across them, and one project's batches stay on one partition so they never run concurrently. Raise it with cores, up to the eight partitions (a ninth consumer would sit idle); `memory` mode always runs one.                                                                                                                                                                        | `4`             |

To use the bundled broker, set the mode and start the stack with its profile:

```bash theme={null}
TESSARY_INGEST_SPOOL_MODE=kafka
```

```bash theme={null}
docker compose --profile kafka up -d
```

Redpanda keeps its log in the `redpanda-data` volume, listens on the internal network only, takes about 512 MB, and runs with write caching off so the acknowledgement the backend waits for means the batch is on disk. Any Kafka-compatible broker you already run works instead: set the bootstrap address and leave the profile off. The topics (`tessary.ingest`, eight partitions keyed by project, and `tessary.ingest.dead-letter` for a batch that exhausted its write retries) are created the first time the backend reaches the broker.

In both modes `/actuator/health` carries an `ingest` component with the buffer's depth, the age of its oldest unprocessed batch, its dead-lettered count and whether the drainer is alive; it reports DOWN when the drainer is dead or the oldest batch is older than `TESSARY_INGEST_SPOOL_MAX_LAG_MS` (five minutes by default, `0` disables).

## Data retention

An hourly sweep deletes traces and classifier detections older than their retention period. The install-wide defaults are environment variables; `0` means keep forever.

| Variable                               | Description                                                          | Default |
| -------------------------------------- | -------------------------------------------------------------------- | ------- |
| `TESSARY_RETENTION_TRACE_TTL_DAYS`     | Days a trace and its content are kept. `0` keeps them forever.       | `90`    |
| `TESSARY_RETENTION_DETECTION_TTL_DAYS` | Days a classifier detection is kept. `0` keeps them forever.         | `90`    |
| `TESSARY_RETENTION_ENABLED`            | The sweep itself. `false` deletes nothing, whatever the periods say. | `true`  |

An owner or admin can override either period for one project under **Settings → Data retention**, which is also `GET` and `PUT /api/orgs/{orgSlug}/projects/{projectSlug}/retention`. The `PUT` body replaces both overrides at once: `{"traces": 30, "detections": null}` sets a 30-day override for traces and clears the detections override, so the install default applies to it again; an absent key clears too. The page shows the install default next to each override, and the sweep enforces exactly what the page shows. A trace that an open finding still cites is kept past its period until the finding closes.

## LLM providers

LLM providers have no environment variables. Configure them in-app, under **Settings** then **Providers**. Tessary seals whatever you enter there with `TESSARY_SECRET_KEY`.

The one exception is an ambient region for Bedrock:

| Variable     | Description                                                             | Default |
| ------------ | ----------------------------------------------------------------------- | ------- |
| `AWS_REGION` | Region Tessary uses for Bedrock calls that don't otherwise specify one. | Blank   |

## Classifier encoder service

| Variable                           | Description                                   | Default |
| ---------------------------------- | --------------------------------------------- | ------- |
| `TESSARY_OBSERVER_ENCODER_URL`     | URL of the classify-service encoder endpoint. | Blank   |
| `TESSARY_OBSERVER_ENCODER_API_KEY` | API key for the encoder endpoint.             | Blank   |

Both ship blank, and in the open edition they stay blank: the model weights classify-service loads are gated and not published, so the frustration and groundedness classifiers are unavailable rather than merely unconfigured. See [Encoder-based classifiers are unavailable in the open edition](/self-hosting/setup#encoder-based-classifiers-are-unavailable-in-the-open-edition) in Setup, and the classifier section of [Troubleshooting](/self-hosting/troubleshooting).

## Agentic launcher and sandbox-runner

| Variable                                | Description                                                                                                                                                                                                    | Default                                               |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `TESSARY_OBSERVER_AGENTIC_LAUNCHER_URL` | URL of the sandbox-runner service that runs agentic analysis (RCA, triage, drift detection).                                                                                                                   | Auto-wired by Compose to `http://sandbox-runner:8080` |
| `SANDBOX_BACKEND`                       | Where sandbox-runner launches sandboxed agent sessions: `docker` (sibling containers), `e2b` (Tessary-hosted microVMs), or `local` (the host's logged-in `claude`).                                            | `docker`                                              |
| `SANDBOX_WORK_VOLUME`                   | Name of the Docker volume sandbox-runner shares with the sibling containers it spawns. The launcher hands this exact string to the Engine API, so it is the volume's real name and not a project-prefixed one. | `tessary-sandbox-work`                                |
| `AGENT_PROVIDER`                        | Where the sandboxed agent runs: unset for SigV4 Bedrock, `anthropic` for the first-party Anthropic API, or `bedrock-api-key` for Bedrock over a bearer token.                                                  | Unset (SigV4 Bedrock)                                 |

## GitHub integration

Connecting a GitHub repository needs no environment variables. Connect one in-app, under **Settings** then **Git integration**, through the manifest wizard or with a personal access token. See [Optional: connect a GitHub repository](/self-hosting/setup#optional-connect-a-github-repository) in Setup.

## Telemetry

| Variable                    | Description                                                | Default |
| --------------------------- | ---------------------------------------------------------- | ------- |
| `TESSARY_TELEMETRY_ENABLED` | Sends one anonymous, opt-out heartbeat to home.tessary.ai. | `true`  |

<Tip>
  Set `TESSARY_TELEMETRY_ENABLED` to `false` for zero outbound network calls, including DNS lookups, to home.tessary.ai.
</Tip>

A default deployment has exactly one outbound destination: `home.tessary.ai`. There is no Google
Fonts call, no analytics SDK, and no other third-party host a default install reaches — the
product's one non-Geist typeface (the wordmark) ships as a local font file, not a call to Google's
CDN. Nothing else in this section is an exception to that; it exists to say precisely what the one
ping carries.

**What the ping sends, once on backend start and then every 24 hours:**

| Field                                       | Example                   | What it is                                                                                  |
| ------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------- |
| `contract_version`                          | `1`                       | Schema version of this payload.                                                             |
| `install_id`                                | a v4 UUID                 | Generated once at first boot. Identifies an install, never a person or an org.              |
| `edition`                                   | `"open"`                  | Which build sent the ping.                                                                  |
| `app_version`                               | `"2026.9.1"`              | The running app's version.                                                                  |
| `os` / `arch`                               | `"linux"` / `"arm64"`     | Host OS family and CPU architecture.                                                        |
| `org_count_bucket` / `project_count_bucket` | `"1-5"`                   | Coarse, bucketed counts — never an exact number.                                            |
| `trace_volume_bucket`                       | `"1k-10k"`                | Coarse, bucketed daily trace-ingest volume — never an exact count, and never trace content. |
| `timestamp`                                 | an ISO-8601 UTC timestamp | When the ping was generated.                                                                |

It never carries an email address, an org or project name, trace/prompt/dataset content, a license
key, or a retained IP address. The full field-by-field contract, including the closed bucket enums,
lives at `devdocs/reference/telemetry-contract.md` in the repository for anyone auditing the wire
format against the running code.

## What leaves your network

The complete set of destinations a self-hosted instance may reach, and when. Anything else is a defect; `scripts/check-zero-egress.sh` in the repository boots the stack on a network with no route out, points every service at a DNS sink that records each name asked for, runs the setup page's steps end to end with the heartbeat turned off, and fails on any name outside this table, on any image pulled after the stack is up, and on any host named anywhere in the files the frontend serves, lazily loaded ones included, that is not on its enumerated list of never-fetched strings. The same run turns the heartbeat back on and requires the one permitted lookup to appear, so the check is known to be able to fail. Two limits the check states rather than hides: the frontend keeps a route out for its published port, so a connection it made to a bare IP address (never a name) would go unobserved, and its only configured egress is certificate issuance; and the sandbox runner pulls the agent image at the first analysis, which the check's walk never reaches, so that pull is listed below rather than measured.

| Destination                                         | When                                                                                                                                                            | Why it is permitted                                                                                                                                                                                                                           |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `home.tessary.ai`                                   | Once at backend start and every 24 hours, only while `TESSARY_TELEMETRY_ENABLED` is `true`                                                                      | The heartbeat above. Off, the name is never even resolved.                                                                                                                                                                                    |
| `docker.io`                                         | `docker compose pull`, before the stack runs; and once more at the first analysis, for the agent image the sandbox runner pulls lazily unless you pull it first | The published images. The check pulls or builds them before its observation window opens, so a pull during operation is a failure; the agent image is the one exception, named here because the check's walk stops before the first analysis. |
| Nothing from the bundled Redpanda                   | Only with `--profile kafka`                                                                                                                                     | Redpanda's metrics reporter (`enable_metrics_reporter`, the one setting that phones home) is off in the flags the profile starts it with.                                                                                                     |
| `acme-v02.api.letsencrypt.org`                      | Only when `SITE_DOMAIN` names a real host                                                                                                                       | Caddy's certificate issuance. Unset, Caddy serves plain HTTP and contacts nobody.                                                                                                                                                             |
| The provider, alert and Git endpoints you configure | Only after you enter a credential in Settings                                                                                                                   | Your own integrations. A default boot has none.                                                                                                                                                                                               |

**Turning it off costs nothing functional.** `TESSARY_TELEMETRY_ENABLED=false` gives Tessary no
install-count or activation visibility into your deployment — that is the entire effect. There is no
license check, feature gate, or in-app behavior that depends on the heartbeat reaching us.

**Tessary will never use your conversations to train or improve any model — no exceptions, no
toggle.** This is an absolute guarantee, not a default you could accidentally switch off. See
`devdocs/reference/principles.md` § "Product & positioning" for the full argument, backed by two
guard tests in the codebase
(`NoSharedModelTrainingGuaranteeTest`, `PriorContributionGuardTest`) that fail the build if this
ever stops being true.

## Ingest and OTLP

Tessary can receive traces directly over OTLP (OpenTelemetry Protocol) instead of pulling them from a connected source.

| Variable                        | Description                                                            | Default |
| ------------------------------- | ---------------------------------------------------------------------- | ------- |
| `TESSARY_INGEST_OTLP_TRANSPORT` | Transport the OTLP ingest endpoint accepts: `HTTP`, `GRPC`, or `BOTH`. | `HTTP`  |
| `TESSARY_INGEST_OTLP_GRPC_PORT` | Port the gRPC transport binds, when enabled.                           | `4317`  |

## TLS and domain

These variables apply to the prod Docker Compose stack (`docker-compose.yml`). The dev stack always serves plain HTTP on port 8000. [Custom domain](/self-hosting/custom-domain) walks through them.

| Variable                    | Description                                                                                                                                                                                                                                                                                                                                                       | Default |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `SITE_DOMAIN`               | The one public-origin variable: a bare hostname such as `tessary.acme-corp.com`. Unset serves plain HTTP on `HTTP_PORT` (default 80) only. Set, it is served the way `TLS_MODE` says, and the sign-in landing origin, the WorkOS callback and the agentic lanes' callback origin are derived from it. A value carrying a scheme, port or path is refused at boot. | Unset   |
| `TLS_MODE`                  | `acme` (Caddy obtains a Let's Encrypt certificate over TLS-ALPN-01), `owncert` (your certificate and key mounted at `/certs/tls.crt` and `/certs/tls.key` in the frontend container), or `upstream` (plain HTTP behind a terminator you run).                                                                                                                     | `acme`  |
| `ACME_EMAIL`                | Your address for the Let's Encrypt account. Required when `SITE_DOMAIN` is set in `acme` mode; there is no default.                                                                                                                                                                                                                                               | Blank   |
| `TRUSTED_PROXIES`           | Space-separated CIDRs of the proxy in front of Tessary in `upstream` mode. Forwarded headers are honoured from these addresses only.                                                                                                                                                                                                                              | Blank   |
| `TESSARY_AUTH_FRONTEND_URL` | Where the browser lands after sign-in and sign-out. Derived from `SITE_DOMAIN` when set; otherwise from `HTTP_PORT`. An explicit value must name the same host as `SITE_DOMAIN`.                                                                                                                                                                                  | Derived |
| `WORKOS_REDIRECT_URI`       | The WorkOS callback, registered in the WorkOS dashboard. Derived from `SITE_DOMAIN` when set.                                                                                                                                                                                                                                                                     | Derived |
| `HTTP_PORT`                 | Host port mapped to the frontend container's port 8000.                                                                                                                                                                                                                                                                                                           | `80`    |
| `HTTPS_PORT`                | Host port for HTTPS, when `SITE_DOMAIN` is set in `acme` or `owncert` mode.                                                                                                                                                                                                                                                                                       | `443`   |

<Note>
  `SITE_DOMAIN` is also how Tessary knows this instance is reachable from outside the host. Setting it while either sealing key is still on its shipped placeholder refuses the boot, naming the key. That is what makes the placeholders safe to ship: they cannot follow you onto a public host.
</Note>

## Removed configuration

<Note>
  These keys and blocks belonged to features Tessary has removed entirely, not merely disabled. Spring ignores an unbound key, so setting one has no effect: it will not turn the feature back on.
</Note>

* The `TESSARY_SANDBOX_*` block
* The `TESSARY_SYNTH_*` block
* `TESSARY_CI_GATE_BLOCK_ON_REGRESSION`
* `TESSARY_RISK_MODEL_ENABLED`
* The `TESSARY_GRADING_*` keys
* `TESSARY_STORAGE_EMBED_ENABLED`
* `TESSARY_EMBEDDING_BASE_URL`
