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:
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 setTESSARY_JDBC_URL directly if you run the backend outside Compose, against a Postgres you manage yourself.
Secrets
Generate a key with:
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
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:
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 withTESSARY_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 infrustration_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
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_REGRESSIONTESSARY_RISK_MODEL_ENABLED- The
TESSARY_GRADING_*keys TESSARY_STORAGE_EMBED_ENABLEDTESSARY_EMBEDDING_BASE_URL
