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 organization, which in the open edition is the whole instance.
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.
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.
The one exception is an ambient region for Bedrock:
Classifier encoder service
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 in Setup, and the classifier section of Troubleshooting.
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 one
ping carries.
What the ping sends, once on backend start and then every 24 hours:
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.
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.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 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.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
