Skip to content
TulipFarmDocs

Last updated

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.

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

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.

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:

Terminal window
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 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.

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

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.

The configuration reference lists every variable by group, with its services, requirement, secrecy, and default.

Was this page helpful?