ChatAI Docs

Environment reference

Full environment variable reference for ChatAI self-hosting and development.

Copy .env.example to .env and fill in values. Docker Compose loads .env for the app service and overrides DATABASE_URL / BETTER_AUTH_URL / UPLOAD_DIR as needed. For a production VPS start from .env.production.example and work through the production checklist.

Variables marked secret must never be committed, logged, or exposed through NEXT_PUBLIC_*. The app validates its environment at startup and refuses to boot when a value is malformed.

Core

VariableRequiredDefault / examplePurpose
NODE_ENVNodevelopment (Docker image: production)Runtime mode
DATABASE_URLYes (runtime)postgresql://chatai:chatai@localhost:5432/chataiPooled or direct Postgres URL for the app. Secret (contains the password)
DATABASE_URL_UNPOOLEDNeon migrations—Direct (non-pooler) URL of the same Neon endpoint as DATABASE_URL; used by migrations and the ingest/eval workers. Migrations refuse a different endpoint unless ALLOW_DATABASE_URL_MISMATCH=1.
BETTER_AUTH_SECRETYes—Auth signing secret (min 16 chars; use openssl rand -base64 32). Secret
BETTER_AUTH_URLYeshttp://localhost:3000Public origin browsers use for this instance
UPLOAD_DIRNo./uploads (Docker: /app/uploads)File upload storage
RUN_MIGRATIONSNo1Docker entrypoint runs migrations before starting. 0 skips them (the VPS deploy script runs them as a separate step)
NEXT_PUBLIC_APP_URL / NEXT_PUBLIC_DOCS_URLNo (build time)http://localhost:3000 / http://localhost:3001Public app and docs origins baked into the image at build (docker build --build-arg). Never put secrets here
SEED_DEMO_ON_STARTNo—When 1, Docker entrypoint seeds demo user after health (dev/demo only; never in production)
SEED_DEMO_EMAIL / SEED_DEMO_PASSWORDNodemo@chatai.local / DemoPass123!Overrides for demo seed

Auth (optional)

VariablePurpose
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRETEnable GitHub OAuth; leave blank to disable
RESEND_API_KEYSend password-reset emails via Resend. Without it, reset links are logged to the server console
EMAIL_FROMFrom address for Resend (e.g. ChatAI <noreply@yourdomain.com>; defaults to Resend sandbox from-address)

Chat providers

VariablePurpose
AI_PROVIDERopenai, openai-compatible, anthropic, google, openrouter, azure, ollama, groq
AI_API_KEYDefault OpenAI-compatible key; Ollama accepts a placeholder like ollama
AI_BASE_URLOpenAI-compatible base URL (default https://api.openai.com/v1)
AI_MODELDefault chat model (e.g. gpt-4o-mini, llama3.2)
OLLAMA_BASE_URLOllama OpenAI-compatible endpoint (Compose: http://ollama:11434/v1)
ANTHROPIC_API_KEYAnthropic
GOOGLE_GENERATIVE_AI_API_KEYGoogle Gemini
OPENROUTER_API_KEYOpenRouter
GROQ_API_KEYGroq
AZURE_OPENAI_API_KEY / AZURE_OPENAI_RESOURCEAzure OpenAI

Missing credentials for the selected provider return 503 (provider X is not configured on this instance).

Embeddings

VariableDefaultPurpose
EMBEDDING_PROVIDERopenaiopenai, openai-compatible, cohere, voyage
EMBEDDING_MODELtext-embedding-3-smallEmbedding model id
EMBEDDING_DIMENSIONS1536Must match chunks.embedding (vector(1536))
VOYAGE_API_KEY—Voyage embeddings
COHERE_API_KEY—Cohere embeddings and optional rerank

Changing embedding width on an existing database requires a full reprocess strategy; the column width is fixed in schema migrations today.

Rate limits and quality

VariableDefaultPurpose
API_RATE_LIMIT_PER_MINUTE60Per-API-key REST limit (widget publicId chat is not limited here)
COHERE_API_KEY—Prefer Cohere rerank when set
OUTPUT_SCOPE_CHECKtrueSelective output scope check: risky turns (partial, classifier fallback, injection signal, weak grounding, flexible mode, recent redirect) are buffered and checked against the assistant's Purpose before delivery. false disables it
ASSISTANT_PROFILEtrueAssistant Profile page (Purpose and owner-reviewed Key facts) and the background Key facts refresh worker. Requires migration 0020
PROFILE_ANSWER_ROUTEfalseAnswer basic identity, contact and hours questions from published Key facts without retrieval. Keep off until its evaluation gate passes for your model

See Scope and Profile.

Security and widget protection

VariableRequiredDefaultPurpose
ENCRYPTION_KEYWhen any assistant stores its own provider key—AES-256-GCM key (32 bytes) for per-assistant provider secrets. Generate with openssl rand -base64 32 or openssl rand -hex 32; optional base64: / hex: prefix. Secret. Back it up: losing it makes stored provider keys unreadable
ENCRYPTION_KEY_VERSIONNo1Version label written into new ciphertext. Bump when rotating
ENCRYPTION_KEY_PREVIOUS / ENCRYPTION_KEY_PREVIOUS_VERSIONDuring rotation—Prior key kept for decryption only until secrets are re-encrypted. Secret
WIDGET_RATE_LIMIT_PER_VISITOR_PER_MINUTENo20Public widget chat limit per visitor. Owners can override per assistant in Settings → Security
WIDGET_RATE_LIMIT_PER_ASSISTANT_PER_MINUTENo120Public widget chat limit per assistant
WIDGET_SIGNING_MAX_SKEW_SECONDSNo300Allowed clock skew, in seconds, for signed widget requests when the assistant requires signing

Hosted usage, billing and admin

Details: Billing and usage.

VariableRequiredDefaultPurpose
HOSTED_USAGE_ENFORCEMENTNoshadowshadow meters without blocking, enforce applies plan limits, off disables metering writes
HOSTED_USAGE_DEFAULT_LIMIT_MICROSNo1000000Fallback monthly provider-cost ceiling in micro-dollars ($1 = 1,000,000) when an account has no plan row
HOSTED_USAGE_MAX_OUTPUT_TOKENSNo4096Output-token ceiling used to estimate hosted chat cost
HOSTED_USAGE_RECONCILE_STALE_MINUTESNo15Age after which abandoned usage reservations are released
HOSTED_USAGE_EXEMPT_PLAYGROUNDNofalsetrue skips hard-limit reservation for owner playground chat (still metered)
ADMIN_USER_IDSNoemptyComma-separated user IDs allowed to call /api/admin/*. Empty means no admins (403 for every signed-in user)
POLAR_ACCESS_TOKENFor paid plans—Polar organization token. Without it billing endpoints return 503. Secret
POLAR_WEBHOOK_SECRETFor paid plans—Verifies /api/webhooks/polar. Secret
POLAR_SERVERNosandboxsandbox or production
POLAR_PRODUCT_ID_STARTER / _PRO / _BUSINESSFor paid plans—Allowlisted Polar Product IDs per plan
POLAR_PRODUCT_ID_TEAMNo—Deprecated alias for Business, used only when POLAR_PRODUCT_ID_BUSINESS is unset

Voice (preview)

Overview and owner settings: Voice (preview).

VariableDefaultPurpose
VOICE_PROVIDERgpt-livegpt-live for real Voice; mock runs without credentials (tests and CI only)
VOICE_OPENAI_API_KEY—GPT-Live key, server-side only. Independent of the text chat provider. Without it Voice is unavailable and text chat is unaffected. Secret
VOICE_OPENAI_BASE_URLhttps://api.openai.comGPT-Live API origin
VOICE_QUOTA_EXEMPT_PLAYGROUNDtrueOwner playground Voice is always measured and reported; when true it is not counted toward the account's Voice-minute quota. Independent of HOSTED_USAGE_EXEMPT_PLAYGROUND (text chat)
VOICE_MAX_CONCURRENT_SESSIONS—Instance-wide cap on concurrent Voice sessions, to stay under the provider organization's concurrency limit. Unset: only per-plan limits apply
VOICE_IDLE_TIMEOUT_SECONDS180Seconds without visitor speech before the assistant checks in; the call ends 30 s later if the visitor stays silent. 0 disables idle ending (the 60-minute cap still applies)
VOICE_SHUTDOWN_GRACE_MS8000Graceful-drain budget on SIGTERM/SIGINT (1000–60000). Live calls are hung up, settled and their recordings finalized within it; then the process exits. Keep the platform stop grace at least 7 s longer
NEXT_MANUAL_SIG_HANDLE— (Docker image: 1)Required for the Voice drain. Without it Next.js exits within ~50 ms of SIGTERM and live calls fall back to crash recovery

Voice runtime state is process-local: run one web instance (or sticky sessions by Voice session id). ChatAI supervises every call server-side: if its provider control link drops it reconnects for up to 15 s before ending the call, stalled lookups get a neutral apology after 15 s, and calls end at 60 minutes (the assistant warns about 30 s before). Voice needs a long-lived Node.js server; see Voice in production.

Voice minutes

Voice usage is measured in seconds per session and follows HOSTED_USAGE_ENFORCEMENT:

  • off: seconds are recorded on each session; no quota and no provider-cost ledger.
  • shadow: seconds and provider cost are recorded; nothing is refused (except VOICE_MAX_CONCURRENT_SESSIONS).
  • enforce: each session reserves up to 5 minutes at a time from the account's Voice-minute allowance for the current period and extends while talking. A session that reaches the end of its allowance is told once, about 30 seconds before, and then closed. New sessions are refused with 402 when fewer than 30 seconds remain, and with 429 when the plan's concurrent-session limit is reached.

A session is billed from the provider's final usage when available, otherwise from ChatAI's last durable checkpoint (marked estimated). Sessions left open by a crashed or restarted process are recovered and settled by the maintenance sweep. Recording and Voice minutes are metered independently.

Voice audio recording (optional)

Recording needs S3-compatible object storage. Without a bucket and credentials, the Record Voice audio setting is disabled and Voice works normally. Recordings stay private: the bucket is never exposed to browsers, and owners play recordings back through an authenticated ChatAI proxy with short-lived links.

VariableDefaultPurpose
OBJECT_STORAGE_BUCKET—Bucket for recordings (keys: voice/<assistantId>/<recordingId>.webm)
OBJECT_STORAGE_REGIONautoRegion (auto for R2; e.g. us-east-1 for S3/MinIO)
OBJECT_STORAGE_ENDPOINT—Custom endpoint for R2 (https://<account-id>.r2.cloudflarestorage.com) or MinIO (http://minio:9000). Omit for AWS S3
OBJECT_STORAGE_ACCESS_KEY_ID—Access key with put/get/head/delete on the bucket
OBJECT_STORAGE_SECRET_ACCESS_KEY—Secret key. Secret
OBJECT_STORAGE_FORCE_PATH_STYLEfalsetrue for MinIO and most self-hosted S3 servers
VOICE_RECORDING_SPOOL_DIR./.voice-recording-spool (Docker: /app/uploads/voice-spool)Local compressed spool for recordings in progress. Put it on persistent disk so a partial recording can be recovered after a crash or restart
VOICE_ALIGNMENT_DIAGNOSTICSoff1: log one numbers-only [voice.alignment] line per recorded call (clock offsets, transcript-vs-audio onset bias). Never logs audio or transcript content

Each recorded Voice session produces one stereo WebM/Opus file (visitor on the left channel, assistant on the right), about 0.36 MB per minute. Only compressed audio touches the spool; raw audio is never written to disk.

Self-hosted MinIO example (add to your Compose file, then create the bucket once):

minio:
  image: minio/minio
  command: server /data
  environment:
    MINIO_ROOT_USER: chatai
    MINIO_ROOT_PASSWORD: change-me-long-secret
  volumes:
    - minio-data:/data
OBJECT_STORAGE_BUCKET=chatai-recordings
OBJECT_STORAGE_REGION=us-east-1
OBJECT_STORAGE_ENDPOINT=http://minio:9000
OBJECT_STORAGE_ACCESS_KEY_ID=chatai
OBJECT_STORAGE_SECRET_ACCESS_KEY=change-me-long-secret
OBJECT_STORAGE_FORCE_PATH_STYLE=true
VOICE_RECORDING_SPOOL_DIR=/app/uploads/voice-spool

Keep the bucket private (no public-read policy). Recording deletion follows the assistant's privacy settings: recordings are deleted with their conversation, by the optional recording retention, and when the assistant is deleted.

Ollama Compose profile

See Local models (Ollama). Summary:

  • Profile: docker compose --profile local-models up
  • Prefer hybrid: Ollama for chat, 1536-d embeddings elsewhere
  • Fully local nomic-embed-text (768-d) is not compatible with vector(1536) yet

Playwright e2e (local / CI)

VariablePurpose
E2E_EMAIL / E2E_PASSWORDSeeded e2e user (pnpm e2e:seed)
E2E_ASSISTANT_IDSeeded assistant public id
E2E_BASE_URLApp origin under test

Do not commit .env.e2e.