Self-hosting configuration
How TulipFarm reads configuration, the operator settings you supply, the settings setup generates, secret files, and encryption key rotation.
TulipFarm reads configuration only from environment variables. Any tool that can set process environment variables can deliver it. See Secret stores.
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,
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
Section titled “Secret files”Every application variable marked secret in the reference 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:
env: - name: ANTHROPIC_API_KEY_FILE value: /var/run/secrets/tulipfarm/anthropic-api-keyvolumeMounts: - name: tulipfarm-secrets mountPath: /var/run/secrets/tulipfarm readOnly: trueA 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
Section titled “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. |
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. 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. 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. |
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 [email protected]. |
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
Section titled “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:
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 ./generatedThe 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 hostTULIPFARM_OBJECT_STORAGE_BUCKET: defaulttulipfarm-soulTULIPFARM_OBJECT_STORAGE_REGION: defaultauto
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
Section titled “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 asv1. 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
Section titled “Rotate encryption keys”before: KEY_ID=v1 KEY=<k1> PREVIOUS_KEYS unsetafter: 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:
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 }- Generate the new key with
openssl rand -hex 32. Store it in your secret store asINTEGRATION_CREDENTIAL_KEY_V2. - Read the current key from
state.json, then storeINTEGRATION_CREDENTIAL_PREVIOUS_KEYS={"v1":"<old key>"}. - Apply the override to every service in the table above for that prefix.
- 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
Section titled “Variable reference”The configuration reference lists every variable by group, with its services, requirement, secrecy, and default.