Configuration
Required and optional environment variables for OrcAI, and the embedding setting instance admins save in the app.
Runtime configuration is loaded across app and package config modules. .env.example is the current baseline for self-hosted environments. Every value comes from the container environment and applies when the container restarts, so one image serves any instance. The one exception is the legal pages, which are compiled into the image. Build args only carry image identity, never deployment values.
Under Docker Compose, a container only sees the variables docker-compose.yaml passes to it. Compose sets the service addresses itself (POSTGRES_HOST, QDRANT_URL, SPICEDB_ENDPOINT) and fills SPICEDB_TOKEN from SPICEDB_GRPC_PRESHARED_KEY. The bundled PostgreSQL always creates the user postgres and the database app, so leave POSTGRES_USER and POSTGRES_DB at those values. Compose does not pass BETTER_AUTH_RATE_LIMIT, WORKER_POLLING_INTERVAL_SECONDS or PORT, so setting them in .env has no effect until you add them to the service's environment.
The primary config modules are:
packages/ai/src/config.tspackages/db/src/config.tspackages/notifications/src/config.tspackages/qdrant/src/config.tspackages/s3/src/server/config.tspackages/spice-db/src/config.tsapps/app/src/lib/effect/services/config.ts
Public URLs
| Variable | Required | Notes |
|---|---|---|
BETTER_AUTH_URL | Yes | The app's public origin, used for auth callbacks and links in emails. Passkeys are bound to its host name, so changing the domain invalidates every registered passkey, and users then sign in with their password and add them again. |
WEB_URL | No | Public docs site base URL used in the app dashboard and in the OrcAI Assistant's docs links. It is also where the Assistant reads the docs unless WEB_INTERNAL_URL is set. Without it the Assistant cannot read the docs. |
WEB_INTERNAL_URL | No | Docs site origin the app server fetches for the Assistant. Defaults to WEB_URL. Docker Compose sets http://web:3001. |
PORT | No | Port the server listens on. Defaults to 3000 for the app and 3001 for the docs site, which their images and the Compose files expect. |
Browsers need to reach three origins: the app at BETTER_AUTH_URL, the docs site at WEB_URL, and object storage at S3_PUBLIC_ENDPOINT (see Object storage). docker-compose.yaml publishes no ports, so put a reverse proxy with TLS in front of these services, or publish their ports the way docker-compose.local.yaml does.
The app sends a Content Security Policy with every response. Browsers then load content only from the app itself, from object storage at S3_PUBLIC_ENDPOINT (or S3_ENDPOINT when it is unset), from YouTube's privacy-enhanced embeds, and from the Umami script's origin when UMAMI_SCRIPT_URL and UMAMI_WEBSITE_ID are set. Both origins come from this configuration, so the policy follows a changed value after a restart. Each response carries its own nonce for the page's scripts, so a reverse proxy must pass the Content-Security-Policy header through unchanged, add no second policy, and not cache pages.
Authentication and encryption
| Variable | Required | Notes |
|---|---|---|
BETTER_AUTH_SECRET | Yes | Better Auth signing secret. |
BETTER_AUTH_RATE_LIMIT | No | false turns off Better Auth's rate limiting, which is on in production by default. It keys clients by X-Forwarded-For, and requests without exactly one address in it share one limit. |
ENCRYPTION_KEY | Yes | Application encryption key. Any string works, but use a random value of 32 to 64 characters. Provider API keys are stored encrypted with it, so the app and the workers need the same value. |
TWO_FACTOR_REQUIRED | No | Who must use two-factor authentication: none (default), admins (instance admins) or all. A covered user without it is sent to set it up before using the app. |
Database and cache
| Variable | Required | Notes |
|---|---|---|
POSTGRES_USER | Yes | PostgreSQL user. |
POSTGRES_PASSWORD | Yes | PostgreSQL password. |
POSTGRES_HOST | Yes | PostgreSQL host. |
POSTGRES_PORT | No | PostgreSQL port, default 5432. |
POSTGRES_DB | Yes | PostgreSQL database name. |
POSTGRES_POOL_MAX | No | The most connections each pool opens, default 10. The app runs three pools (queries, Better Auth, pg-boss) and the workers two (queries, pg-boss), so keep the total for all processes below PostgreSQL's max_connections (100 by default). |
WORKER_POLLING_INTERVAL_SECONDS | No | Replaces how often every background worker polls its queue, at least 0.5. Unset, each worker keeps its own interval of 2 to 30 seconds. |
Object storage
| Variable | Required | Notes |
|---|---|---|
S3_ENDPOINT | Yes | S3-compatible endpoint the app and workers use. |
S3_PUBLIC_ENDPOINT | No | Endpoint in the presigned URLs that browsers upload to and download from. Defaults to S3_ENDPOINT. Set it when browsers cannot reach S3_ENDPOINT. |
S3_REGION | No | Defaults to eu-central-1. |
S3_ACCESS_KEY | Yes | S3 access key. With the bundled SeaweedFS, it also sets the server's admin key. |
S3_SECRET_KEY | Yes | S3 secret key. With the bundled SeaweedFS, it also sets the server's admin secret. |
Browsers send files straight to object storage and download them from it, so S3_PUBLIC_ENDPOINT must be reachable from every user's browser. OrcAI addresses buckets by path (https://storage.example/uploads/...), not by subdomain. It uses two buckets, uploads and processed-files, with these fixed names, and creates them when they are missing, so the access key needs the right to create buckets unless you create both first. When object storage runs on a different origin than the app, its CORS rules must allow PUT with any request header from the BETTER_AUTH_URL origin and expose the ETag header, which multipart uploads read. The bundled SeaweedFS creates both buckets on startup.
AI and retrieval
| Variable | Required | Notes |
|---|---|---|
KREUZBERG_OCR_LANGUAGE | No | OCR languages for asset preparation. Defaults to eng in host-oriented code and eng+deu in Compose examples. |
No environment variable sets the embedding endpoint. An instance admin sets it in the app, as described under Embeddings. Chats, the Assistant and the utility model use the providers each organisation adds in the app.
Embeddings
The embedding endpoint and model are the same for every organisation on the instance and are stored in the database. An instance admin sets them under Manage instance > Settings (/instance/settings), in the Embeddings section:
- Base URL, the address of any OpenAI-compatible endpoint, usually ending in
/v1. - API key, stored encrypted with
ENCRYPTION_KEYand never shown again. Leaving it empty when you edit the setting keeps the stored key. - Model, the embedding model. Load models reads the endpoint's
/modelslist, and for an endpoint without a list you enter the model name. - Query prefix and Passage prefix, fixed text some models expect before every search query or before every passage they embed. The model's documentation names it, and both stay empty for models that need none. OrcAI keeps spaces and line breaks as entered. E5 instruct models such as
e5-mistral-7b-instruct, for example, expect a query prefix likeInstruct: Given a question, retrieve passages that answer it, a line break, andQuery:. Other E5 models expectquery:andpassage:.
Save and test runs a test embedding first. If it fails, nothing is saved and the page shows the reason: the endpoint rejected the key, could not be reached, does not know the model, or answered with something other than a vector. If it succeeds, OrcAI reads the dimensions from the test vector, so there is no field for them.
Each embedding model has its own Qdrant collection, named orcai-chunks-g<generation>-<model>-<dimensions>. The first save creates it and embeds every asset in a repository again from its stored extracted text, so no file is read again and no image is described again. Changing the API key, the base URL or the query prefix while keeping the model applies at once, without embedding anything again. The passages stored for search and keyword matching stay without the passage prefix.
Change the model
Saving another model, the same model name with other dimensions, or another passage prefix asks for confirmation first and states how many assets in repositories it embeds again. Once confirmed, search moves to a new, empty collection at once, the old collection is deleted, and the workers embed every asset in a repository again from its stored extracted text. Search finds only the assets already embedded with the new setting until all of them are. Meanwhile, chats that search repositories and repository pages show a notice that search is being rebuilt. There is no way back to the old vectors, so going back to the old model embeds everything again too. If another instance admin changed the setting after you opened the page, saving fails and asks you to reload the page and confirm again.
The Search index section below the setting counts the assets in repositories per organisation: embedded with the current model, waiting, and failed, with the failure reasons as counts. It names no file or repository. While assets wait, and after Prepare assets again until they are queued, it refreshes every few seconds.
- Retry failed assets embeds the failed assets again from their stored text and leaves every embedded asset as it is. It skips assets whose stored text is missing.
- Prepare assets again appears when assets failed because their stored text is missing, as with files prepared before OrcAI described images. The workers then queue each of them once to be prepared again in the background, so they read the file again, describe its images with the utility model of the asset's organisation and embed it in every repository that holds it. Assets already being prepared are skipped. While they are prepared, their repository links count as waiting. Until the asset is embedded again, search keeps finding it in the repositories where it was already embedded.
- Re-embed everything embeds every asset in a repository again with the current setting, into a new collection, for when the endpoint's vectors changed without a new model name.
All three ask for confirmation with the number of assets first. Rate limits of the endpoint count as temporary failures, which the workers retry with a delay before an asset counts as failed.
Retry failed assets and Prepare assets again show up in your Activity as one run each. A request to prepare assets again while another one is still queueing joins that run instead of starting a second one.
Until embeddings are set up, uploads still work and OrcAI prepares the assets, but their indexing shows "Waiting for the embedding setup" and repository search is unavailable. Instance admins see a banner in the app that links to Settings, and the Health page fails its Embeddings check. Once the setting is saved, the workers index every waiting asset within about a minute, so nothing has to be uploaded again.
An instance that set its embedding model through environment variables keeps its vectors in a collection named orcai-chunks-<dimensions>. After the update, search is unavailable until an instance admin saves the embedding setting. Saving embeds every asset in a repository again into the new collection and deletes the old one. OrcAI does not read the old embedding variables, so remove them from .env.
Qdrant
| Variable | Required | Notes |
|---|---|---|
QDRANT_URL | Yes | Qdrant HTTP endpoint. |
QDRANT_API_KEY | Yes | Qdrant API key. Local development may use a placeholder. |
SpiceDB
| Variable | Required | Notes |
|---|---|---|
SPICEDB_ENDPOINT | Yes | SpiceDB gRPC endpoint. |
SPICEDB_TOKEN | Yes | Token used by the app. |
SPICEDB_SECURITY | No | Transport mode for the SpiceDB client: secure, insecure-localhost or insecure-plaintext (default). |
SPICEDB_READ_TIMEOUT | No | How long a permission check may wait for SpiceDB before the request answers 503, default 5 seconds. |
SPICEDB_GRPC_PRESHARED_KEY | Compose | Used by the Compose-managed SpiceDB service. |
SPICEDB_POSTGRES_USER | Compose | User for the Compose-managed SpiceDB database. |
SPICEDB_POSTGRES_PASSWORD | Compose | Password for the Compose-managed SpiceDB database. |
Only the workers read these variables, because they send every email. Until SMTP is configured, email stays in log-only mode for local testing and nothing is delivered. The workers then log each complete rendered message, including password reset, verification and invitation links, so do not use log-only mode in production and treat any captured log output as sensitive credentials.
| Variable | Required | Notes |
|---|---|---|
SMTP_HOST | No | Must be set with SMTP_FROM to enable SMTP. |
SMTP_PORT | No | Defaults to 587 when SMTP mode is enabled and no port is set. |
SMTP_USERNAME | No | Must be paired with SMTP_PASSWORD if used. |
SMTP_PASSWORD | No | Must be paired with SMTP_USERNAME if used. |
SMTP_SECURE | No | If unset, derived from port (true for 465, otherwise false). |
SMTP_TLS_REJECT_UNAUTHORIZED | No | Defaults to true. |
SMTP_FROM | No | Must be set with SMTP_HOST to enable SMTP. |
SMTP_FROM_NAME | No | Defaults to OrcAI Team. |
Observability
| Variable | Required | Notes |
|---|---|---|
UMAMI_SCRIPT_URL | No | Umami script URL. Analytics load only when UMAMI_WEBSITE_ID is set too, and setting only one of them logs a warning. |
UMAMI_WEBSITE_ID | No | Umami website ID. |
FEEDBACK_EMAIL | No | Address the last card of the first tour names for feedback. Without an address, or with an invalid one that logs a warning, the card says nothing about feedback. |
OTEL_EXPORTER_OTLP_ENDPOINT | No | OpenTelemetry export endpoint. |
OTEL_EXPORTER_OTLP_HEADERS | No | OpenTelemetry export headers. |
OTEL_RESOURCE_ATTRIBUTES | No | Resource attributes attached to every exported signal, for example deployment.environment=production. |
Legal pages
The privacy policy at /privacy and the terms of use at /tou are Markdown files compiled into the app image: apps/app/src/static/legal/privacy-policy.md and apps/app/src/static/legal/terms-of-use.md. Both are templates that ask the operator to add its name, contact details and the legal terms that apply. Edit them and build the image again before users create accounts. The first-run form at /init and the account creation form link to them.