# Self-hosting configuration

> How TulipFarm reads configuration, the operator settings you supply, the settings setup generates, secret files, and encryption key rotation.

Source: https://tulipfarm.ai/docs/self-hosting/configuration/

TulipFarm reads configuration only from environment variables. Any tool that can set
process environment variables can deliver it. See [Secret stores](https://tulipfarm.ai/docs/self-hosting/secrets/index.md).

[`packages/config/src/settings.ts`](https://github.com/TulipFarm/tulipfarm/blob/main/packages/config/src/settings.ts) declares
every variable: its services, whether it is required or secret, and its default. The
operator table on this page, the [configuration reference](https://tulipfarm.ai/docs/self-hosting/configuration-reference/index.md),
and `self-host/.env.example` are generated from it with `pnpm config:docs`; a repository test fails when they are stale or when source code
reads a variable the registry does not declare.

## Secret files

Every application variable marked secret in the [reference](https://tulipfarm.ai/docs/self-hosting/configuration-reference/index.md) accepts
`<NAME>_FILE`: a path to a file that holds the value. The api, worker, storage-worker,
snippet-broker, and `self-host:setup` read the file at startup and strip trailing
newlines. This is the Docker and Kubernetes secret-file convention, so a mounted
secret works without a wrapper. For example, in a Kubernetes container:

```yaml
env:
  - name: ANTHROPIC_API_KEY_FILE
    value: /var/run/secrets/tulipfarm/anthropic-api-key
volumeMounts:
  - name: tulipfarm-secrets
    mountPath: /var/run/secrets/tulipfarm
    readOnly: true
```

A process stops at startup when the file cannot be read, or when `<NAME>` and
`<NAME>_FILE` are both set with different values. The errors name the variable and
path, never the value. Compose interpolation of `self-host/.env` does not read
`_FILE` variables, and the bundled `self-host/compose.yaml` passes its required
values directly; use `_FILE` when you run the images with your own manifests.

## Operator settings

Set these yourself. `self-host/.env.example` lists them with comments.

| Name | Required | Used by | Purpose |
| --- | --- | --- | --- |
| `TULIPFARM_PUBLIC_URL` | Yes | api, web build | Public HTTPS origin, such as `https://tulipfarm.example.com`. |
| `TULIPFARM_SITE_ADDRESS` | Yes | web | Caddy site: a domain for automatic HTTPS, or `:80` behind another proxy. |
| `TULIPFARM_RELEASE_VERSION` | Yes | image builds | `YYYY.M.D.N`, a valid date. |
| `TULIPFARM_RELEASE_COMMIT` | Yes | image builds | Full 40-character source commit. |
| `POSTGRES_PASSWORD` | Yes | postgres, setup | PostgreSQL superuser password; use URL-safe characters. |
| `OBJECT_STORAGE_ACCESS_KEY_ID` | Yes | object-storage, setup | Bundled object-storage access key. |
| `OBJECT_STORAGE_SECRET_ACCESS_KEY` | Yes | object-storage, setup | Bundled object-storage secret key. |
| `TULIPFARM_HTTP_PORT` | No | web | Host HTTP port. Default `80`. |
| `TULIPFARM_HTTPS_PORT` | No | web | Host HTTPS port. Default `443`. |
| `TULIPFARM_SUPPORT_EMAIL` | No | web build | Support address on the Help page (web `VITE_SUPPORT_EMAIL`). Unset hides the contact section. |
| `COMPOSE_PROFILES` | No | docker compose | Compose profiles to start. `temporal` runs the bundled Temporal server, UI, schema and namespace jobs; omit it when TEMPORAL_ADDRESS points at your own Temporal. |
| `TEMPORAL_UI_PORT` | No | temporal-ui | Loopback port for the bundled Temporal UI. Default `8233`. |
| `TEMPORAL_HISTORY_SHARDS` | No | temporal | Bundled Temporal history shard count. You cannot change it after first start. Default `128`. |
| `TEMPORAL_UI_URL` | No | api | Temporal UI URL probed by the API health check. Empty skips the probe. Self-host defaults to the bundled `temporal-ui`. Default `http://127.0.0.1:8233`. |
| `PUBLIC_WEB_ENDPOINT` | No | api, worker, database | Origin (no path or credentials) of a keyless Firecrawl-compatible API used by the agent `web.search` and `web.fetch` tools: `POST /v2/search`, `POST /v2/scrape`. Unset turns public web retrieval off; the tools then report results as unavailable. When set, the API health check needs `GET /health` to return 2xx, and `pnpm dev:setup` copies an exported value into the worker env. The endpoint fetches URLs that agents choose, so it must not be able to reach your internal network. |
| `TEMPORAL_ADDRESS` | No | api, worker, storage-worker, database | Temporal frontend address (`host:port`). Self-host defaults to the bundled `temporal:7233`; set it when COMPOSE_PROFILES omits `temporal`. No TLS or API-key client options yet. Default `127.0.0.1:7233`. |
| `TEMPORAL_NAMESPACE` | No | api, worker, storage-worker, database | Temporal namespace. The bundled namespace job creates it. Default `default`. |
| `ANTHROPIC_API_KEY` | One provider key | api, worker, storage-worker | Anthropic API key for Claude model families. |
| `OPENAI_API_KEY` | One provider key | api, worker, storage-worker | OpenAI API key for GPT model families on api.openai.com. Azure Foundry serves them instead when `AZURE_FOUNDRY_API_KEY` is set. |
| `XAI_API_KEY` | One provider key | api, worker, storage-worker | xAI API key for Grok model families on api.x.ai. Azure Foundry serves them instead when `AZURE_FOUNDRY_API_KEY` is set. |
| `GEMINI_API_KEY` | One provider key | api, worker, storage-worker | Google Gemini API key for Gemini model families. |
| `ANTHROPIC_BASE_URL` | No | api, worker, storage-worker | Replaces the Anthropic API origin, for a gateway or regional endpoint; `/v1/messages` is appended, so leave `/v1` out. Its host must be in `MODEL_ENDPOINT_HOSTS`. Default `https://api.anthropic.com`. |
| `OPENAI_BASE_URL` | No | api, worker, storage-worker | Replaces the OpenAI API base URL, for a gateway such as LiteLLM or Portkey. A URL without a path gets `/v1`; one with a path is the API base. Its host must be in `MODEL_ENDPOINT_HOSTS`. Default `https://api.openai.com/v1`. |
| `XAI_BASE_URL` | No | api, worker, storage-worker | Replaces the xAI API base URL, with the same rules as `OPENAI_BASE_URL`. Its host must be in `MODEL_ENDPOINT_HOSTS`. Default `https://api.x.ai/v1`. |
| `GEMINI_BASE_URL` | No | api, worker, storage-worker | Replaces the Gemini API origin; `/v1beta/models/...` is appended, so leave `/v1beta` out. Its host must be in `MODEL_ENDPOINT_HOSTS`. Default `https://generativelanguage.googleapis.com`. |
| `MODEL_ENDPOINT_HOSTS` | With a custom model endpoint | api, worker, storage-worker | Comma-separated host names (no scheme, port or path; exact match, no wildcards) that model credentials may be sent to, besides the built-in provider APIs and Azure Foundry resource hosts. Required for every `*_BASE_URL` on another host and every `openai-compatible` catalog endpoint. Endpoints use https; http is allowed only for localhost, 127.0.0.1 and ::1 outside production. URLs must not contain credentials. |
| `MODEL_FAMILIES` | No | api, worker, storage-worker | Comma-separated model families to enable. Unset enables every family a configured provider key serves. See [model configuration](https://tulipfarm.ai/docs/self-hosting/models/index.md). |
| `MODEL_DEFAULT_FAMILY` | No | api, worker, storage-worker | Default model family for new agents and agent-instruction improvements. Falls back to the catalog's `default-chat` role. |
| `MODEL_CATALOG_FILE` | No | api, worker | Path to a JSON model catalog (`{ "families": [...], "models": [...] }`, the shape of the shipped `model-families.json` entries and `model-pricing.json` models). A model entry with a shipped ID sets your own prices; a family with `"pricing": "unmetered"` has models without rates that cost zero. A family's `roles` (`default-chat`, `title`, `compaction`, `review`, `plan`, `heartbeat`, `public-guide`) make it the default for those features. Startup fails naming any invalid entry. Set this or `MODEL_CATALOG`, not both. |
| `MODEL_CATALOG` | No | api, worker | Inline JSON model catalog, the same shape as the `MODEL_CATALOG_FILE` contents. |
| `MODEL_CATALOG_MODE` | No | api, worker | `extend` adds the operator catalog to the shipped one; an entry with a shipped ID replaces it. `replace` uses only the operator catalog. `MODEL_FAMILIES` still selects the enabled families. Default `extend`. |
| `AZURE_FOUNDRY_ENDPOINT` | Conditional | api, worker, storage-worker | Azure Foundry endpoint for Azure model families and knowledge embeddings. Required with `AZURE_FOUNDRY_API_KEY`; there is no built-in endpoint. |
| `AZURE_FOUNDRY_API_KEY` | One provider key | api, worker, storage-worker | Azure Foundry API key for Azure model families, and for GPT and Grok families when it is set. |
| `KNOWLEDGE_EMBEDDINGS_ENABLED` | No | api, worker, storage-worker | `true` embeds knowledge with the configured embeddings provider. Default `false`. |
| `KNOWLEDGE_EMBEDDINGS_PROVIDER` | No | api, worker, storage-worker | `azure-foundry` or `openai-compatible`. Either must serve `text-embedding-3-large` at 1536 dimensions. See [embedding providers](https://tulipfarm.ai/docs/self-hosting/models/index.md#embedding-providers). Default `azure-foundry`. |
| `KNOWLEDGE_EMBEDDINGS_BASE_URL` | Conditional | api, worker, storage-worker | OpenAI-compatible base URL; `embeddings` is appended. HTTPS, or HTTP on loopback only. |
| `KNOWLEDGE_EMBEDDINGS_API_KEY` | Conditional | api, worker, storage-worker | API key for the OpenAI-compatible embeddings provider. |
| `KNOWLEDGE_EMBEDDINGS_MODEL` | No | api, worker, storage-worker | Model or deployment name sent to the provider. Defaults to the stored profile model. |
| `KNOWLEDGE_EMBEDDINGS_DIMENSIONS` | No | api, worker, storage-worker | Must equal 1536 when set. Default `1536`. |
| `GOOGLE_CLIENT_ID` | One sign-in method | api | Google sign-in client ID. |
| `GOOGLE_CLIENT_SECRET` | One sign-in method | api | Google sign-in client secret. |
| `TURNSTILE_SITE_KEY` | No | api | Cloudflare Turnstile site key for sign-in. Unset disables the challenge. |
| `TURNSTILE_SECRET_KEY` | No | api | Cloudflare Turnstile secret key. Set together with TURNSTILE_SITE_KEY. |
| `AUTH_EMAIL_PASSWORD` | No | api | `true` adds email and password sign-in next to email links and Google, for instances without an email transport. Without one, an admin runs `pnpm auth:reset-password <email>` for a reset link. See [password sign-in](https://tulipfarm.ai/docs/self-hosting/sign-in/index.md#password-sign-in). Default `false`. |
| `ACS_CONNECTION_STRING` | One sign-in method | api, worker | Azure Communication Services connection string for email sign-in and invitations. |
| `ACS_SENDER_ADDRESS` | One sign-in method | worker | Verified sender address for email. |
| `EMAIL_PROVIDER` | No | api, worker | `acs` or `smtp`. Unset uses ACS when `ACS_CONNECTION_STRING` is set, otherwise SMTP when `SMTP_HOST` is set. Any email setting disables local dev sign-in. See [email transports](https://tulipfarm.ai/docs/self-hosting/email/index.md#choose-a-transport). |
| `SMTP_HOST` | With SMTP | api, worker | SMTP server host name. |
| `SMTP_PORT` | No | worker | Defaults to `465` with `SMTP_SECURE=true`, otherwise `587`. |
| `SMTP_SECURE` | No | worker | `true` for implicit TLS. Defaults to `true` only on port 465. |
| `SMTP_REQUIRE_TLS` | No | worker | Require STARTTLS without implicit TLS. Set `false` only for a trusted local relay. Default `true`. |
| `SMTP_USER` | No | worker | SMTP user name. Set with `SMTP_PASSWORD`. |
| `SMTP_PASSWORD` | No | worker | SMTP password. Set with `SMTP_USER`. |
| `SMTP_FROM_ADDRESS` | With SMTP | worker | Sender address, for example `noreply@example.com`. |
| `FEATURE_FLAG_SOUL_REPOSITORY_EXPLORER` | No | api, worker | `true` or `false` sets the `soul-repository-explorer` flag when the flag provider gives no answer: an admin-only, permission-filtered, read-only explorer in workspace settings. Default `on outside production`. |
| `FEATURE_FLAG_PUBLIC_TULIPFARM_GUIDE` | No | api, worker | `true` or `false` sets the `public-tulipfarm-guide` flag when the flag provider gives no answer: Public website guide, bounded model answers and inline signup. Default `on outside production`. |
| `FEATURE_FLAG_GOOGLE_CALENDAR_CANVAS` | No | api, worker | `true` or `false` sets the `google-calendar-canvas` flag when the flag provider gives no answer: Calendar read and action canvas cards and approved Google Meet creation. Default `on outside production`. |
| `FEATURE_FLAG_SLACK_APP_SENDER` | No | api, worker | `true` or `false` sets the `slack-app-sender` flag when the flag provider gives no answer: Workflow and agent Slack sends can post as the TulipFarm app instead of the requester. Default `on outside production`. |
| `FEATURE_FLAG_DEFAULT_CONNECTION_KNOWLEDGE` | No | api, worker | `true` or `false` sets the `default-connection-knowledge` flag when the flag provider gives no answer: New eligible integration enablements automatically get owner-only Knowledge with ongoing discovery. Default `on outside production`. |

## Generated settings

`setup` generates these and writes them to per-service env files. You do not set them.
To run TulipFarm without Compose, run the setup command and deliver each file's
values to the matching service:

```sh
TULIPFARM_DATABASE_ADMIN_URL='postgresql://admin:…@db.example.com:5432/postgres?sslmode=require' \
TULIPFARM_OBJECT_STORAGE_ENDPOINT=https://s3.example.com \
TULIPFARM_OBJECT_STORAGE_ACCESS_KEY_ID=… \
TULIPFARM_OBJECT_STORAGE_SECRET_ACCESS_KEY=… \
pnpm self-host:setup --out ./generated
```

The [`apps/database/Dockerfile`](https://github.com/TulipFarm/tulipfarm/blob/main/apps/database/Dockerfile) image runs the same command. Optional inputs are:

- `TULIPFARM_DATABASE_HOST`: the host that services use, if it differs from the admin URL host
- `TULIPFARM_OBJECT_STORAGE_BUCKET`: default `tulipfarm-soul`
- `TULIPFARM_OBJECT_STORAGE_REGION`: default `auto`

Query parameters on the admin URL, such as `sslmode`, are copied to every generated URL.

| File | Service | Contents |
| --- | --- | --- |
| `state.json` | none | The source of truth: generated passwords, keys, and the auth secret |
| `api.env` | api | `DATABASE_URL`, `BETTER_AUTH_SECRET`, encryption keys, workspace-storage logins, object storage |
| `worker.env` | worker | Encryption keys, authoring login, `EMAIL_DELIVERY_DATABASE_URL`, object storage |
| `storage-worker.env` | storage-worker | Encryption keys, provisioning and migration logins, object storage |
| `temporal.env` | temporal, temporal-schema | Temporal database login and TLS settings |

Each service also needs the non-secret settings in `self-host/compose.yaml`, such as
`TEMPORAL_ADDRESS`, `NODE_ENV=production` from the image, and `API_PROXY_MODE`.

## Encryption keys

TulipFarm encrypts stored credentials with AES-256-GCM. Each key has a name prefix:

| Prefix | Protects | Services |
| --- | --- | --- |
| `WORKSPACE_RUNTIME` | Workspace database runtime credentials | api, worker, storage-worker |
| `WORKSPACE_CONFIRMATION` | Table-confirmation credentials | api, storage-worker |
| `WORKSPACE_MIGRATION` | Workspace migration credentials | storage-worker |
| `EMAIL_DELIVERY` | Queued sign-in links and invitations | api, worker, storage-worker |
| `INTEGRATION_CREDENTIAL` | Connected-integration tokens | api, worker, storage-worker |

Each prefix uses three variables:

- `<PREFIX>_KEY_ID`: the current key ID, such as `v1`. It is stored with each ciphertext.
- `<PREFIX>_KEY`: the current key, 64 hexadecimal characters (32 bytes).
- `<PREFIX>_PREVIOUS_KEYS`: optional JSON that maps old key IDs to old keys. Used only to decrypt.

### Rotate encryption keys

```text
before:  KEY_ID=v1  KEY=<k1>  PREVIOUS_KEYS unset
after:   KEY_ID=v2  KEY=<k2>  PREVIOUS_KEYS={"v1":"<k1>"}
```

New writes use `v2`, and existing `v1` data still decrypts. TulipFarm has no
re-encryption command yet, so keep previous keys until all old data is gone.
All five prefixes share one implementation, `@tulipfarm/keyring`, which also reports
the key ID of each stored value for a future re-encryption pass.

With Compose, `setup` always writes key ID `v1` from `state.json`. Container variables
take precedence over the generated file, so rotate with
`self-host/compose.override.yaml`. Compose loads that file automatically:

```yaml
x-integration-key: &integration-key
  INTEGRATION_CREDENTIAL_KEY_ID: v2
  INTEGRATION_CREDENTIAL_KEY: ${INTEGRATION_CREDENTIAL_KEY_V2:?}
  INTEGRATION_CREDENTIAL_PREVIOUS_KEYS: ${INTEGRATION_CREDENTIAL_PREVIOUS_KEYS:?}
services:
  api: { environment: *integration-key }
  worker: { environment: *integration-key }
  storage-worker: { environment: *integration-key }
```

1. Generate the new key with `openssl rand -hex 32`. Store it in your secret store as
   `INTEGRATION_CREDENTIAL_KEY_V2`.
2. Read the current key from `state.json`, then store
   `INTEGRATION_CREDENTIAL_PREVIOUS_KEYS={"v1":"<old key>"}`.
3. Apply the override to **every** service in the table above for that prefix.
4. Run `docker compose up -d`.

The new key exists only in your secret store, so back it up there.

`BETTER_AUTH_SECRET` and generated database passwords do not support rotation yet.
Do not change them after first start.

## Variable reference

The [configuration reference](https://tulipfarm.ai/docs/self-hosting/configuration-reference/index.md) lists every
variable by group, with its services, requirement, secrecy, and default.
