Skip to main content
LangWatch is configured through environment variables. How you set them depends on your deployment method:
  • Docker Compose: Set in your .env file
  • Helm chart: Set via values.yaml (the chart maps values to env vars automatically)
  • Raw Kubernetes: Set directly in your Deployment manifests
When using the Helm chart, you rarely need to set environment variables directly. The values.yaml file provides a structured way to configure everything. See the Helm chart mapping table below.

Core Configuration

There is no in-process worker mode. The app and the background workers are always separate processes/pods. See Architecture and the workers.* Helm values below.

Secrets

The AI Gateway uses three more secrets, LW_GATEWAY_INTERNAL_SECRET, LW_GATEWAY_JWT_SECRET and LW_VIRTUAL_KEY_PEPPER. Set all three or none. See AI Gateway security.
Never commit secrets to version control. In production, use a secrets manager (AWS Secrets Manager, HashiCorp Vault) or Kubernetes Secrets with secretKeyRef in the Helm chart.

License

Authentication

A single sign-on connection is set up per organization in Settings > Authentication, with no environment variables. See SSO Configuration. Installs that configure a deployment-wide provider with AUTH_PROVIDER and its provider variables keep working; see Upgrading SSO.

Restricting sign-up

By default anybody who can reach the install can create an account and then create an organization of their own. To close that:
  • SIGN_UP_MODE=invite_only allows an account only for an address with a pending invitation. Members join the organizations that invited them. Only the operators in ADMIN_EMAILS can create a new organization, apart from the first organization on an install that has none.
  • SIGN_UP_ALLOWED_DOMAINS allows an account only for the listed email domains. It works with either mode.
An invited address can always sign up, whatever its domain, from the invitation link or from the sign-in screen. Either way it lands on its invitation. An address in ADMIN_EMAILS can always sign up. The first administrator on an invite-only install:
  1. Set ADMIN_EMAILS to your address, then sign up with it. No other address can sign up without an invitation.
  2. Create the first organization and invite the rest of your team from Settings > Members.
With ADMIN_EMAILS empty, the first account on an install with no users is still allowed, so the install can be set up. When SIGN_UP_ALLOWED_DOMAINS is set, that first account also has to be on a listed domain. Anybody who reaches the install before you can take that account, so set ADMIN_EMAILS on an install that is reachable from outside your network. The restriction applies to email and password sign-up, passkey sign-up, and first sign-in through a deployment-wide identity provider (AUTH_PROVIDER). It does not apply to an organization’s own single sign-on connection or to SCIM provisioning: those are the organization’s decision. Existing accounts are not affected.

Network and Access

Services

LANGWATCH_NLP_INTERNAL_SECRET is optional, and how you provide it depends on the deployment:
  • Docker Compose: the app, the workers and langwatch_nlp all read .env, so one line covers every container. Generate a value with openssl rand -hex 32. The NLP service is not published on a host port, so only the compose network reaches it.
  • Helm chart: the chart generates the value into the app Secret and points the app, the workers and the NLP Deployment at the same key, so a default install needs no change. If you supply the app Secret yourself, add a LANGWATCH_NLP_INTERNAL_SECRET key to it (or rename the key with langwatch_nlp.secrets.internalSecretKey) and roll the three Deployments.

Object Storage (S3)

The dataplane S3 bucket is the general file-storage layer for all externalized byte content. Current consumers:
  • Stored objects: externalized byte content (audio, image, video, document) extracted from incoming events and dataset uploads. Bytes are content-addressed under {projectId}/{sha256} and served back via GET /api/v1/files/:id.
  • Dataset uploads: persists rows uploaded through the dataset UI. Shares the same S3_BUCKET_NAME bucket.
When S3_BUCKET_NAME is set, all consumers use that bucket. When it is not set, stored-objects fall back to the local filesystem at LANGWATCH_LOCAL_STORAGE_PATH. That is fine for single-replica installs, not for horizontally-scaled deployments (see warning below).
When running on AWS with IRSA (IAM Roles for Service Accounts), you don’t need to set S3 access keys. The pod’s service account assumes the IAM role automatically.
LANGWATCH_LOCAL_STORAGE_PATH is single-replica only. Multi-pod Kubernetes deployments must NOT rely on it: pods do not share a local filesystem, so a write from pod A is invisible to pod B and bytes vanish on every pod restart.Single-replica self-host installs (small footprints, hobbyist / air-gapped / pre-pilot deployments) can use it safely: the Helm chart refuses to render localFilesystem.enabled=true together with replicaCount > 1 so the misconfiguration can’t reach a cluster. Use S3_BUCKET_NAME (or the equivalent Helm app.dataplane.enabled toggle) for any horizontally-scaled deployment.

Object Storage (Azure Blob)

Azure Blob Storage is an alternative to S3 for the same dataplane consumers (stored objects + dataset uploads). It is selected only by the explicit STORED_OBJECTS_BACKEND=azure toggle. Setting the AZURE_BLOB_* variables below by themselves has no effect. This is deliberate (issue #4133): deployment behavior must not depend on which env vars happen to be set, so a stray AZURE_BLOB_ACCOUNT_KEY left over from a decommissioned integration can never silently redirect writes. Precedence, evaluated in this order: a per-project BYOC S3 bucket (if configured for that project) beats the azure toggle; the azure toggle beats S3_BUCKET_NAME; S3_BUCKET_NAME beats the local-filesystem fallback. This azure backend covers datasets too: the destination resolver is shared by both write paths.
When STORED_OBJECTS_BACKEND=azure is set, AZURE_BLOB_ACCOUNT_NAME and AZURE_BLOB_CONTAINER are required, plus AZURE_BLOB_ACCOUNT_KEY when AZURE_BLOB_AUTH_MODE is sharedKey (the default). Under an identity mode the key must be absent (see below).An incomplete active Azure configuration fails with an error and never falls back to S3 or the local filesystem. Inactive Azure settings are validated only when an azure-blob:// address is used, so they cannot break global or private-bucket S3 traffic.Set AZURE_BLOB_ENDPOINT (chart: app.dataplane.providers.azureBlob.endpoint) for sovereign clouds or private endpoints; it defaults to https://{accountName}.blob.core.windows.net. Per-project Azure BYOC is tracked separately (issue #6088).

Migrating an existing installation

Do not switch STORED_OBJECTS_BACKEND before copying existing data. The migration task uses its own OBJECT_STORAGE_MIGRATION_* credentials, so source reads and destination writes do not depend on the app’s active provider:
plan is read-only. copy is resumable, verifies SHA-256 at the destination, and repairs a mismatched destination object. finalize refuses to run unless both reads and writes are explicitly paused, dataset uploads are idle, and GroupQueue has no pending, delayed, active, blocked, or staged durable references. Reads must remain paused because finalization publishes destination storage addresses before the target-provider deployment is active. It publishes newer ClickHouse stored-object rows only after the final verified copy; it does not rewrite or delete history. No phase deletes source bytes. After finalize succeeds, update STORED_OBJECTS_BACKEND and the active provider credentials while traffic remains paused, deploy that configuration, run verify, then resume traffic. A global migration excludes projects with private S3 buckets; they remain on their tenant-owned S3 destination. Keep the source provider’s read credentials configured through the rollback window. Finalization rewrites stored-object addresses to the destination scheme, so a rollback that reverts STORED_OBJECTS_BACKEND and removes the destination provider’s credentials leaves every rewritten address unreadable. The bytes still exist on both providers (no phase deletes them), but the deployment can no longer serve that scheme. Concretely:
  • S3 → Azure: after cutover, keep S3_BUCKET_NAME + S3 credentials configured (Helm: app.dataplane.legacyS3ReadBucket) so pre-migration s3:// history stays readable. If you roll back to S3, keep the AZURE_BLOB_* settings configured (Helm: app.dataplane.legacyAzureRead: true) so the finalized azure-blob:// addresses stay readable.
  • Azure → S3: the mirror image. Keep AZURE_BLOB_* (Helm: legacyAzureRead) after cutover, and keep the S3 settings if you roll back.
A rollback with the flag set is fully non-destructive: reads resolve per address scheme, and a reverse migration can later re-home the bytes. Also note: the plan reports rows addressed by a scheme outside both providers (for example file:// from a local-storage era). Those rows are out of the migration’s scope, stay on their current addresses, and remain readable as long as that scheme’s backend stays configured.

Authenticating without an account key

AZURE_BLOB_AUTH_MODE selects how the app proves who it is. It defaults to sharedKey, so existing installs are unaffected. Many organisations set allowSharedKeyAccess=false on the storage account, frequently enforced through Azure Policy. On such an account the shared-key mode does not degrade: every request is refused. Use one of the identity modes instead: Whichever identity you use, grant it Storage Blob Data Contributor on the storage account or the specific container. The control-plane Contributor role does not grant data access. This is the single most common setup mistake, and it surfaces as a permission error rather than a sign-in error. Three rules before you configure it:
  • There is no fallback. If an identity mode is selected and the identity is missing or unauthorised, storage fails with an error and stays down until it is fixed. It never quietly reverts to an account key, because that would reverse your security decision at the moment you are least likely to notice.
  • An account key alongside an identity mode is rejected. A key that would be silently ignored is worse than no key, since you would believe it was in use.
  • Bearer tokens require HTTPS. A plaintext AZURE_BLOB_ENDPOINT is refused before any token is sent.
In the chart, set app.dataplane.providers.azureBlob.authMode and enable global.serviceAccount (create: true plus the azure.workload.identity/client-id annotation). The chart refuses to render workloadIdentity without a ServiceAccount, since the identity would have no ServiceAccount to bind to. Federated Kubernetes identity is supported on AKS only, because the token injection depends on the Azure workload-identity webhook. On clusters without it, use managedIdentity or sharedKey. On AKS, three things must line up or the token exchange fails with AADSTS70021 before any storage call is made. The app surfaces that code and this remedy in the error, but they are easier to get right up front:
  • The federated identity credential’s issuer must match the cluster’s OIDC issuer exactly, including the trailing slash. Read it with az aks show --query oidcIssuerProfile.issuerUrl. If the cluster is ever recreated the issuer changes and every federated credential silently stops working.
  • Its subject must be system:serviceaccount:<namespace>:<service-account-name>, matched case-exactly against the ServiceAccount the chart creates.
  • Its audience must be api://AzureADTokenExchange.
The pod label and the ServiceAccount annotation are both emitted by the chart; the federated credential itself is configured on the Azure side and is the piece the chart cannot do for you. The ServiceAccount and the workload-identity label are applied to the app and the workers only. Cron pods call the app over HTTP and never touch storage themselves, so giving them a Blob-capable token would grant every cron image storage access it has no use for. The same identity must be available to any out-of-band job that writes bytes, such as the dataset backfill task; run it with the same ServiceAccount as the app and workers.

Email

Notification emails (alerts, invites, scheduled reports, password resets) are sent through one gateway, selected with EMAIL_PROVIDER. See Email for choosing a gateway and setting one up. When EMAIL_PROVIDER is not set, the gateway is inferred from whichever credentials are present (USE_AWS_SES first, then SENDGRID_API_KEY), so existing installs keep working untouched. smtp and resend must always be named explicitly.

SendGrid

AWS SES

Credentials come from the standard AWS chain (instance role, IRSA, or AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY).

SMTP

Use this for an internal relay, Azure Communication Services, Office 365, Google Workspace, Postmark, Mailgun, Brevo, or any other gateway that speaks SMTP.

Resend

Sending through an outbound proxy

Deployments whose only egress is a corporate HTTP proxy can set the standard HTTPS_PROXY (or HTTP_PROXY) and NO_PROXY variables. The ses, resend and sendgrid gateways all route their API calls through it; NO_PROXY entries are honoured, including leading-dot domain suffixes and *. The smtp gateway ignores these variables on purpose, because an SMTP relay is normally an internal host that must be reached directly. Point it at your relay’s real address, or use a vendor’s SMTP endpoint if that is what the proxy allows out.
Neither the AWS SDK nor Node’s HTTP client reads HTTPS_PROXY on its own, so LangWatch attaches the proxy to the SES and Resend clients itself. SendGrid’s client is axios-based and reads the variables directly. Without this wiring, a proxied deployment fails with a read ETIMEDOUT reaching the provider.

PII Redaction (Google DLP)

Model providers for evaluators, including Azure OpenAI, are configured per project in Settings > Model Providers, not through environment variables.

Voice Agents

VOICE_CALL_MAX_SECONDS and VOICE_RUNS_MAX_CONCURRENT shape voice agent runs, which need FEATURE_FLAG_FORCE_ENABLE=release_voice_agents_enabled. The other three act on every worker boot, with the flag on or off: each worker listens on VOICE_WS_PORT. On a self-hosted production install it opens a cloudflared quick tunnel only when VOICE_TUNNEL=true and VOICE_PUBLIC_BASE_URL is unset.

Upgrade

The app and the workers never migrate on startup: pnpm task upgrade does, before they start, and they refuse to start until it has run. See Run the upgrade. Exit codes, outcome codes and the log lines are in Upgrade Guide.

Cloud admin

Telemetry

Langy (optional)

Langy, the automated AI engineer, runs as a separate agent pod that the chart deploys by default (langyagent.chartManaged: true). It uses a shared LANGY_INTERNAL_SECRET (plus optional GITHUB_LANGY_* and mirror-lane vars). Those are documented on their own page.
See Langy environment variables for the full LANGY_* and GITHUB_LANGY_* reference, and Setup for the langyagent.chartManaged toggle and the LANGY_INTERNAL_SECRET secret.

Helm Chart Mapping

When using the Helm chart, configuration is set in values.yaml rather than environment variables directly. Here’s how key values map:
For production, use secretKeyRef instead of inline values. This references a Kubernetes Secret:
Last modified on October 12, 2026