Environment variables
Every setting in .env.example, generated from it.
This page is generated from .env.example.
Docker Compose users set these in docker/.env (created by sh docker/init-env.sh); Helm users put them
in the Secret passed as existingSecret. Example values for secrets are not shown.
Core
| Variable | Example | What it does |
|---|---|---|
DATABASE_URL | (secret) | The app connects as qaitai_app (no superuser/BYPASSRLS, DML only) so row-level security applies (packages/db/drizzle/0002_rls.sql, docker/sql/app-role.sql). Migrations use the owner role. |
DATABASE_MIGRATION_URL | (secret) | |
BETTER_AUTH_SECRET | openssl rand -base64 32 | |
BETTER_AUTH_URL | http://localhost:3000 | |
PUBLIC_APP_URL | https://app.tryqaitai.com | cloud; self-hosters set their own |
QAITAI_MASTER_KEY | 32 random bytes, base64. Wraps the data keys for BYOK keys and test secrets. Back it up; losing it loses those secrets. openssl rand -base64 32 | |
QAITAI_DEFAULT_PLAN | self_hosted | Plan for new workspaces: self_hosted (no limits, default) | free | pro | team | enterprise. Cloud sets free. |
QAITAI_ADMIN_EMAILS | Instance admins (verified emails, comma-separated). Empty = the first user who signed up. | |
QAITAI_ALLOW_SIGNUP | false | Self-hosted sign-up is invite-only (first account + pending invitations). Set true to allow anyone who can reach this instance to create an account. |
QAITAI_INTERNAL_SECRET | Shared by web + platform runners: they call POST /api/internal/run-completed with it. openssl rand -hex 32 (>= 32 chars) | |
QAITAI_EDITION | selfhost | cloud = qaitai Cloud (tryqaitai.com): domain verification is always enforced and self-hosted runs need BYOK. selfhost (default) = your own instance. |
QAITAI_REQUIRE_DOMAIN_VERIFICATION | true | Cloud-runtime runs may only target verified domains. Self-hosted instances may set false (ignored when QAITAI_EDITION=cloud). Default: true. |
QAITAI_RUNNER_JOB_SECRET | Optional: HMAC key for per-job runner tokens (qjt_). Defaults to a key derived from BETTER_AUTH_SECRET. | |
QAITAI_RATE_LIMIT_STORE | memory | Rate-limit counters (runner API, /api/v1, media): memory (default, per web process) or postgres (shared across web replicas; set this when running more than one web instance, e.g. qaitai Cloud or Helm replicas > 1). |
GitHub App (sign-in + repo access + PR checks)
| Variable | Example | What it does |
|---|---|---|
GITHUB_CLIENT_ID | Easiest: create it in-app (Settings → GitHub, /api/setup/github/manifest) or with scripts/create-github-app.mjs. Anything set here overrides the in-app config. | |
GITHUB_CLIENT_SECRET | ||
GITHUB_APP_ID | ||
GITHUB_APP_SLUG | ||
GITHUB_APP_PRIVATE_KEY | PEM, single line with \n escapes | |
GITHUB_WEBHOOK_SECRET | ||
QAITAI_PREVIEW_HOST_SUFFIXES | vercel.app,netlify.app,pages.dev,onrender.com,fly.dev,up.railway.app | Deployments on these hosts from a connected repo count as verified preview domains. |
Other sign-in
| Variable | Example | What it does |
|---|---|---|
GOOGLE_CLIENT_ID | ||
GOOGLE_CLIENT_SECRET | ||
QAITAI_PASSWORD_BREACH_CHECK | Email + password accounts must verify their email before signing in when a mail transport is set up (always on qaitai Cloud). Reject passwords found in Have I Been Pwned (only a 5-char SHA-1 prefix is sent). Default: true when QAITAI_EDITION=cloud, false self-hosted. |
Email (magic links, invitations, notifications)
| Variable | Example | What it does |
|---|---|---|
EMAIL_PROVIDER | EMAIL_PROVIDER: resend (qaitai Cloud) | smtp (self-hosted) | log (dev only: prints emails, refuses in production). Default: smtp when SMTP_URL/SMTP_HOST is set, else resend when RESEND_API_KEY is set, else log. | |
EMAIL_FROM | default: qaitai <no-reply@BETTER_AUTH_URL host> | |
RESEND_API_KEY | ||
SMTP_URL | Dev: pnpm infra:up runs Mailpit, SMTP on localhost:1026, inbox at http://localhost:8025 smtp://localhost:1026 for Mailpit; or the parts below | |
SMTP_HOST | ||
SMTP_PORT | 587 | |
SMTP_USER | ||
SMTP_PASS | ||
SMTP_SECURE | true = implicit TLS (default when SMTP_PORT=465) | |
QAITAI_DISABLE_BOOTSTRAP | false | Self-hosted, zero users: the web container prints a one-time admin sign-in link at start. true = don't. |
Jobs (Hatchet)
| Variable | Example | What it does |
|---|---|---|
HATCHET_CLIENT_TOKEN | ||
HATCHET_CLIENT_TLS_STRATEGY | none |
Runner
| Variable | Example | What it does |
|---|---|---|
QAITAI_RUNNER_MODE | platform | platform = our own workers: Hatchet + DATABASE_URL + QAITAI_MASTER_KEY (everything above). remote = customer runners: only QAITAI_URL + QAITAI_TOKEN (qrt_... from the Workers page); no DB, no master key, no Hatchet token. Default: remote when QAITAI_TOKEN is set, else platform. |
QAITAI_URL | Remote mode only: the control plane's URL and this runner's token. | |
QAITAI_TOKEN | ||
QAITAI_RUNTIME | cloud | platform mode: cloud | self_hosted (self_hosted also takes self-hosted runs) |
QAITAI_SLOTS | 4 | |
AI_GATEWAY_API_KEY | Managed model keys (BYOK keys are stored per workspace instead). Set either; AI Gateway wins when both are set (provider fallbacks + spend observability), otherwise runs call Anthropic directly. | |
ANTHROPIC_API_KEY | ||
QAITAI_WEB_URL | http://localhost:3000 | Where the runner reaches the web app for run-completion callbacks (internal URL is fine). |
QAITAI_PLATFORM_JOBS | true | false on customer-hosted runners: they execute runs only, not platform cron jobs (domain re-verification). |
STEEL_BASE_URL | http://localhost:3100 | Steel: set STEEL_BASE_URL for the local open-source instance (no key), or STEEL_API_KEY for Steel Cloud. Leave both empty to use local Chromium (npx playwright install chromium). |
STEEL_API_KEY | ||
STEEL_PUBLIC_URL | Browser-reachable base URL for the Steel session viewer on the Live run page, when it differs from STEEL_BASE_URL (how the runner reaches Steel), e.g. https://steel.example.com. Set on the runner. |
Cost model (COGS in usage_events.cost_micros; docs/pricing-and-costs.md#cost-model)
| Variable | Example | What it does |
|---|---|---|
STEEL_COST_PER_HOUR | 0.08 | Steel Cloud price per browser-hour on OUR STEEL_API_KEY. Zero cost for BYO browser keys, open-source Steel (STEEL_BASE_URL without our key), local Chromium and customer (remote) runners. |
QAITAI_INFRA_COST_PER_MIN | 0.001 | Compute + storage + DB overhead per agent-minute, platform-mode runners only (0 on customer runners). |
Notifications & issue trackers (docs/self-hosting.md#notifications-and-integrations)
| Variable | Example | What it does |
|---|---|---|
SLACK_CLIENT_ID | Slack app (OAuth "Add to Slack", threads, Rerun / Assign buttons). Without these, admins can still paste a Slack incoming-webhook URL (one message per run). Redirect URL: <app>/api/integrations/slack/callback; interactivity request URL: <app>/api/integrations/slack/interactions. | |
SLACK_CLIENT_SECRET | ||
SLACK_SIGNING_SECRET | verifies interactivity requests (Rerun / Assign) | |
LINEAR_CLIENT_ID | Linear OAuth app (redirect URL: <app>/api/integrations/linear/callback). Without these, admins paste a Linear personal API key instead. | |
LINEAR_CLIENT_SECRET |
Billing (qaitai Cloud only; docs/pricing-and-costs.md#billing-pipeline)
| Variable | Example | What it does |
|---|---|---|
STRIPE_SECRET_KEY | Billing runs only when QAITAI_EDITION=cloud AND STRIPE_SECRET_KEY is set. Self-hosted instances leave all of these empty: no Stripe calls, no plan limits, /api/stripe/webhook returns 404. Prices and meters are created by pnpm --filter @qaitai/billing stripe:setup and found by lookup key (<STRIPE_LOOKUP_KEY_PREFIX><plan><item>), so no price or meter ids go here. sk_test_... in dev/staging; restricted keys work (customers, checkout, portal, subscriptions, prices, meter events, invoices) | |
STRIPE_WEBHOOK_SECRET | whsec_... of the endpoint https://<app>/api/stripe/webhook (or stripe listen) | |
STRIPE_LOOKUP_KEY_PREFIX | default qaitai | |
STRIPE_AUTOMATIC_TAX | true = Stripe Tax on Checkout (configure tax settings in Stripe first) | |
STRIPE_API_BASE | dev/tests only: http://localhost:12111 for stripe-mock |
Public REST API /api/v1 (docs: API reference)
| Variable | Example | What it does |
|---|---|---|
QAITAI_API_RATE_LIMIT | Requests per minute per API key; empty = plan default (Free 60, Pro 300, Team 600, Enterprise/self-hosted 1200). Counted per web process: behind N replicas a key gets up to N x this. | |
QAITAI_API_IP_RATE_LIMIT | 600 | Requests per minute per client IP before the key is checked (brute-force guard). |
Storage
| Variable | Example | What it does |
|---|---|---|
S3_ENDPOINT | http://localhost:9000 | |
S3_BUCKET | qaitai | |
S3_ACCESS_KEY_ID | (secret) | |
S3_SECRET_ACCESS_KEY | (secret) | |
QAITAI_RECORDING | full | Replays (docs/self-hosting.md#recordings-replays): full | screenshots | off; retention unset = forever. |
QAITAI_RECORDING_RETENTION_DAYS |
Enterprise (ee/, commercial license; everything else works without these)
| Variable | Example | What it does |
|---|---|---|
QAITAI_LICENSE_KEY | Signed license key for self-hosted Enterprise features (SSO, SCIM, custom roles, audit export). Verified offline; see docs/self-hosting.md#enterprise. | |
QAITAI_SSO_TRUSTED_ORIGINS | Origins of identity providers on a private network that SSO may reach (comma-separated). | |
QAITAI_AUDIT_RETENTION_DAYS | Runner: days to keep audit log entries for self-hosted workspaces (unset = forever). |
Telemetry (docs/self-hosting.md#telemetry)
| Variable | Example | What it does |
|---|---|---|
QAITAI_TELEMETRY | Each integration starts only when its own endpoint/key is set. QAITAI_TELEMETRY=off disables ALL of them (OTLP, Sentry, Langfuse, PostHog) whatever else is set; =on also allows PostHog on self-hosted. | |
OTEL_EXPORTER_OTLP_ENDPOINT | OpenTelemetry traces (web request → createRun → Hatchet → runner → agent steps → model → browser). e.g. http://localhost:4318 (Jaeger: docker compose -f docker/compose.dev.yml --profile observability up -d jaeger) | |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | full traces URL, overrides the one above | |
OTEL_EXPORTER_OTLP_HEADERS | k=v,k2=v2 (e.g. an API key header); secret | |
OTEL_SERVICE_NAME | default qaitai-web / qaitai-runner | |
QAITAI_TRACE_SAMPLE_RATE | 0..1 of new traces (default 1) | |
SENTRY_DSN | Sentry errors (web server, browser, runner); events are scrubbed of secrets, bodies, cookies, auth headers. | |
SENTRY_ENVIRONMENT | ||
SENTRY_RELEASE | ||
LANGFUSE_PUBLIC_KEY | Langfuse LLM traces (model calls, tokens, cost; redacted prompts for managed runs and opted-in BYOK orgs). | |
LANGFUSE_SECRET_KEY | ||
LANGFUSE_BASE_URL | default https://cloud.langfuse.com (LANGFUSE_HOST also accepted) | |
LANGFUSE_SAMPLE_RATE | 0..1 of run traces (default 1) | |
POSTHOG_KEY | PostHog product analytics (qaitai Cloud; self-hosted only with QAITAI_TELEMETRY=on). No session recording. | |
POSTHOG_HOST | default https://us.i.posthog.com |