Skip to main content

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:
The sections below group the variables most deployments need to set or understand.
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.

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.

Secrets

Generate a key with:
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. Tessary warns on every boot that runs the default, and refuses to start once SITE_DOMAIN is set.
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

TESSARY_AUTH_COOKIE_PASSWORD is the most urgent of the three 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.
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.

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 first organization created on the install and governs the whole instance, so a self-hoster who signs up first owns it. Anyone the policy admits later gets an organization of their own, and that organization sees the policy read-only, named with the organization that governs it. 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 of the governing organization: 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. To use the bundled broker, set the mode and start the stack with its profile:
The broker and the larger backend heap that goes with it need a bigger host than the default configuration assumes. Host sizing states the smallest one to run this mode on. 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. 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. A TypeSafe key serves only the frustration classifier. An OpenRouter key serves triage, RCA, and the frustration classifier. The one exception is an ambient region for Bedrock:

Classifier encoder service

Frustration shadow model

Optional. A second decision model every Frustration turn is re-sent to after the configured provider has scored it, with both answers kept side by side in frustration_shadow and nothing a customer sees changed. It is how a self-hosted model is measured against the provider on real traffic before it replaces anything. The backlog is every assessment whose request body retention has not yet removed and that has no shadow row; a server that is down just leaves it waiting. To read the comparison, count frustration_shadow rows by frustrated against reference_frustrated; the sweep also logs the running totals as frustration.shadow.sweep. The groundedness model needs a GPU: a Mac with Apple silicon for development, or a GPU instance on AWS for production. The setup file your coding agent follows sets these on a Mac, or prints them for you to add on AWS: on a Mac or on AWS. The frustration classifier does not use this server; it needs an OpenRouter or TypeSafe key, stored under Settings then Providers. See Groundedness needs a GPU in Setup.

Agentic launcher and sandbox-runner

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 in Setup.

Telemetry

Set TESSARY_TELEMETRY_ENABLED to false for zero outbound network calls, including DNS lookups, to home.tessary.ai.
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 backend sends there and fetches from there. What the ping sends, once on backend start and then every 6 hours: It never carries an email address, an org or project name, trace/prompt/dataset content, a license key, or a retained IP address. What the backend fetches, on the same schedule: the model price book. It requests /v1/pricing/manifest.json, a few hundred bytes naming the current book’s hash, and downloads the book itself (about 2 MB) only when that hash is one it does not already hold. It checks the download against the hash before using it. Neither request carries a body or any identifier. New prices apply to spans ingested afterwards; costs already recorded never change. The full field-by-field contract 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. The machine that runs the groundedness model makes its own downloads, separate from Tessary’s containers: serve.py from GitHub, the Python packages it runs on, and the model’s weights from Hugging Face. See What is sent, and to whom. Turning it off costs one thing: price updates between releases. TESSARY_TELEMETRY_ENABLED=false gives Tessary no install-count or activation visibility into your deployment, and your instance stops checking for newer model prices, so it prices from the book bundled in the release you run until you upgrade. There is no license check, feature gate, or other in-app behavior that depends on 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.

Ingest and OTLP

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

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 walks through them.
SITE_DOMAIN is also how Tessary knows this instance is reachable from outside the host. Setting it while any of the three placeholder keys is still on its shipped value refuses the boot, naming the key. That is what makes the placeholders safe to ship: they cannot follow you onto a public host.

Removed configuration

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.
  • 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