This guide walks you through deploying your own instance of Open-Inspect using Terraform.
Looking for local development setup (without full infra deployment)? Start with SETUP_GUIDE.md.
Important: This system is designed for single-tenant deployment only. All users share the same GitHub App credentials and can access any repository the App is installed on. See the Security Model for details.
Open-Inspect uses Terraform to automate deployment across multiple cloud providers:
| Provider | Purpose | What Terraform Creates |
|---|---|---|
| Cloudflare | Control plane, session state | Workers, KV namespaces, Durable Objects, D1 Database |
| Vercel or Cloudflare Workers | Web application | Project + env vars (Vercel) or Worker via OpenNext (Cloudflare) |
| Modal, Daytona, Vercel Sandboxes, OpenComputer, or E2B | Sandbox execution infrastructure | Modal app deployment, Daytona/Vercel API config, OpenComputer template/API config, or E2B template/API config |
Web platform choice: Set
web_platformin yourterraform.tfvarsto"vercel"(default) or"cloudflare". The Cloudflare option deploys the Next.js app as a Cloudflare Worker using OpenNext, so you don't need a Vercel account.
Your job: Create accounts, gather credentials, and configure one file (terraform.tfvars).
Terraform's job: Create all infrastructure and configure services.
Create accounts on these services before continuing:
| Service | Purpose |
|---|---|
| Cloudflare | Control plane hosting (+ web app if using Cloudflare platform) |
| Vercel (optional) | Web application hosting (only if web_platform = "vercel") |
| Modal (optional) | Sandbox infrastructure when sandbox_provider = "modal" |
| Daytona (optional) | Sandbox infrastructure when sandbox_provider = "daytona" |
| Vercel Sandboxes (optional) | Sandbox infrastructure when sandbox_provider = "vercel" |
| OpenComputer (optional) | Sandbox infrastructure when sandbox_provider = "opencomputer" |
| E2B (optional) | Sandbox infrastructure when sandbox_provider = "e2b" |
| GitHub | OAuth + repository access |
| Anthropic | Claude API |
| Slack (optional) | Slack bot integration |
| GitHub App Webhooks (optional) | GitHub bot (PR reviews) |
# Terraform (1.9.0+)
brew install terraform
# Node.js (22+)
brew install node@22
# Python 3.12+ and uv (Modal CLI is installed via uv sync below)
brew install python@3.12 uv
# Wrangler CLI (for initial R2 bucket setup)
npm install -g wranglerFork ColeMurray/background-agents to your GitHub account or organization.
# Clone your fork
git clone https://github.com/YOUR-USERNAME/background-agents.git
cd background-agents
npm install
# Build the shared package (required before Terraform deployment)
npm run build -w @open-inspect/shared
# Install Python dependencies for Modal deployment (includes sandbox-runtime)
cd packages/modal-infra && uv sync --frozen && cd -Tip: Before proceeding, copy
terraform/environments/production/terraform.tfvars.exampletoterraform.tfvarsand keep it open. As you collect credentials in the following steps, paste them directly into this file.
- Go to Cloudflare Dashboard
- Note your Account ID (visible in the dashboard URL or account overview)
- Note your Workers subdomain: Go to Workers & Pages → Overview, look in the bottom-right
of the panel for
*.YOUR-SUBDOMAIN.workers.dev - Create API Token at API Tokens:
- Use template: "Edit Cloudflare Workers"
- Verify it has these permissions:
- Account | Workers KV Storage | Edit (should be included with template)
- Account | Workers R2 Storage | Edit (should be included with template)
- Account | D1 | Edit
- Account | Queues | Edit (required for durable image-build finalization)
- Set "Account Resources" to include your account
- Set "Zone Resources" to include all zones from your account
- Click "Continue to summary" and "Update token"
- Enable R2: Must add payment info, but first 10 GB/month is free
Terraform needs a place to store its state. We use Cloudflare R2.
# Login to Cloudflare
wrangler login
# Create the state bucket
wrangler r2 bucket create open-inspect-terraform-stateCreate an R2 API Token:
- Go to R2 → Overview → Manage R2 API Tokens
- Create token with Object Read & Write permission
- Note the Access Key ID and Secret Access Key
Skip this section if you're deploying the web app to Cloudflare Workers. Important: Do not set
vercel_api_tokenorvercel_team_idto empty strings in yourterraform.tfvars— leave them unset so the dummy defaults are used. The Vercel Terraform provider validates the token on init even when no Vercel resources are created.
- Go to Vercel Account Settings → Tokens
- Create a new token with full access
- Note your Team/Account ID:
- Go to Settings (Account Settings or Team Settings)
- Look for "Your ID" or find it in the URL:
vercel.com/{YOUR_TEAM_ID}/... - Even personal accounts have an ID (usually starts with
team_)
Only required when
sandbox_provider = "modal".
- Go to Modal Settings
- Create a new API token: Settings -> API Tokens -> New Token
- Note the Token ID and Token Secret
- Note your Workspace and Environment name (visible in your Modal dashboard URL, https://modal.com/apps/<modal_workspace>/<modal_environment>)
- Note the environment's Web suffix from Modal's environment settings. Use the normalized
lowercase suffix made of letters, digits, and dashes. Leave it empty for the environment whose
endpoints use
https://<workspace>--...modal.run.
Only required when
sandbox_provider = "daytona".
- Create a Daytona account and generate an API key with the following
permissions:
- Sandboxes: Read, Write (runtime sandbox management and preview URLs)
- Snapshots: Read, Write, Delete (automated snapshot builds via Terraform)
- Note the API URL (e.g.,
https://app.daytona.io/api) and optional target - Seed the named base snapshot before pointing traffic at Daytona:
After initial setup, Terraform automatically rebuilds the snapshot when source files change.
cd packages/daytona-infra pip install daytona # or: uv pip install daytona python -m src.bootstrap --force
- Set
sandbox_provider = "daytona"interraform.tfvars - Set
daytona_api_url,daytona_api_key, anddaytona_base_snapshotinterraform.tfvars
The control plane calls the Daytona REST API directly — no shim service to deploy.
Important: Unlike Modal, the Daytona provider does not automatically inject LLM API keys into sandboxes. If you plan to use Claude models, add
ANTHROPIC_API_KEYas a global secret in Settings > Secrets after deploying. See Secrets Management for details.
Only required when
sandbox_provider = "vercel".
- Create a Vercel API token that can access your sandbox project.
- Note the Project ID for the project that will own sandbox sessions.
- Note the Team/Account ID if you use a Vercel team. Leave it unset for personal accounts where the token can access the project directly.
- Set
sandbox_provider = "vercel"interraform.tfvars. - Set
vercel_sandbox_token,vercel_sandbox_project_id, and optionallyvercel_sandbox_team_idinterraform.tfvars.
The control plane calls the Vercel Sandbox API directly from Cloudflare Workers. No Modal-style shim
service is deployed. Vercel supports filesystem snapshots and prebuilt images; if you have a
reusable base snapshot, set vercel_base_snapshot_id to use it instead of Terraform's managed base
snapshot build.
When Terraform runs with sandbox_provider = "vercel", it builds a managed immutable Vercel
base-runtime snapshot from the checked-out sandbox runtime and Vercel bootstrap source, then passes
a deterministic snapshot name into the Worker deployment. The control plane resolves that name to
the latest created Vercel snapshot at sandbox creation time. The vercel_base_snapshot_id setting
is still available as a manual override. See Vercel Sandbox Provider
for the full runtime, snapshot, and resource configuration model.
Important: Unlike Modal, the Vercel provider does not automatically inject LLM API keys into sandboxes. If you plan to use Claude models, add
ANTHROPIC_API_KEYas a global secret in Settings > Secrets after deploying. See Secrets Management for details.
Only required when
sandbox_provider = "opencomputer".
- Create an OpenComputer API key.
- Set
sandbox_provider = "opencomputer"interraform.tfvars. - Set
opencomputer_api_urlandopencomputer_api_key. - Leave
opencomputer_template = ""to let Terraform build the OpenInspect runtime template, or set it to an existing OpenComputer template name. - Run
terraform apply.
For the full template build and runtime details, see OpenComputer Sandbox Provider.
Only required when
sandbox_provider = "e2b".
E2B needs a single credential — the API key (e2b_api_key). The control plane uses it at
runtime for the E2B REST API, and the e2b-infra module uses it to build the sandbox template (via
the E2B Template SDK). Create it at the E2B dashboard → API Keys.
- Set
sandbox_provider = "e2b"interraform.tfvars. - Set
e2b_api_keyande2b_template_id(e.g.open-inspect-sandbox). - Terraform's
e2b-inframodule builds the template automatically onterraform apply, and rebuilds it whenpackages/e2b-infraorpackages/sandbox-runtimechange. To build manually:cd packages/e2b-infra uv sync --frozen E2B_API_KEY=e2b_… E2B_TEMPLATE_ID=open-inspect-sandbox uv run python build-template.py
The control plane calls the E2B REST API directly from Cloudflare Workers. Each session runs in a
single long-lived sandbox: when its TTL (e2b_sandbox_timeout_seconds, default 7200) expires the
sandbox is paused rather than killed (e2b_auto_pause, default true), so sessions survive idle
gaps; the next prompt resumes it through the control plane. On the Hobby tier (~1h runtime cap)
lower e2b_sandbox_timeout_seconds to 3300. Set e2b_auto_pause = false to kill on timeout
instead.
For the full runtime, lifecycle, and configuration model, see E2B Sandbox Provider.
Important: The E2B provider does not automatically inject LLM API keys into sandboxes. If you plan to use Claude models, add
ANTHROPIC_API_KEYas a global secret in Settings > Secrets after deploying. See Secrets Management for details.
- Go to Anthropic Console
- Create an API key
- Note the API Key (starts with
sk-ant-)
Want to use your OpenAI ChatGPT subscription? See Using OpenAI Models for setup instructions (can be configured after deployment).
Want to use your xAI SuperGrok subscription? See Using Grok with a SuperGrok Subscription. Grok is opt-in and can also be configured after deployment.
Every deployment needs one GitHub App for repository access. The same App can also provide GitHub OAuth sign-in, but its client pair is optional when Google is the only sign-in provider.
-
Go to GitHub Apps
-
Click "New GitHub App"
-
Fill in the basics:
- Name:
Open-Inspect-YourName(must be globally unique) - Homepage URL: Your web app URL (see below)
- Webhook: Leave "Active" unchecked for now. Step 7c enables it when
enable_github_bot = truefor GitHub automations or bot commands.
- Name:
-
If enabling GitHub sign-in, configure Identifying and authorizing users (OAuth):
- Callback URL:
{your-web-app-url}/api/auth/callback/github
Your web app URL depends on
web_platform:- Vercel:
https://open-inspect-{deployment_name}.vercel.app - Cloudflare:
https://open-inspect-web-{deployment_name}.{your-subdomain}.workers.dev - Cloudflare with
cloudflare_custom_domainset:https://{your-custom-domain}
Important: The callback URL must match your deployed web app URL exactly. The
{deployment_name}is the unique value you set interraform.tfvars(e.g., your GitHub username or company name).Keep "User-to-server token expiration" active (GitHub App → Optional Features; it is the default for newly created Apps, but activate it if yours predates that default). Expiring user tokens are what make GitHub return a refresh token at sign-in, and Open-Inspect stores that per-user credential for attributed GitHub operations such as pull-request creation. Clone, fetch, and push authentication still use the shared GitHub App installation. With expiration deactivated — or on an OAuth App, which never issues a refresh token — no per-user credential is captured, so supported attributed operations fall back to the shared GitHub App bot identity.
- Callback URL:
-
Set Repository permissions:
- Actions: Read-only (required for GitHub workflow-run automations)
- Checks: Read-only (required for GitHub check-suite automations)
- Contents: Read & Write
- Issues: Read & Write (required if enabling GitHub bot)
- Pull requests: Read & Write (also authorizes creating and applying labels to session-created pull requests)
- Metadata: Read-only
-
If using
ALLOWED_GITHUB_ORGS/allowed_github_orgs, set Organization permissions:- Members: Read-only
- For existing GitHub Apps, republish the permission change and request/approve installation updates before testing org membership sign-in.
-
If GitHub sign-in uses
allowed_emailsorallowed_email_domains, set Account permissions:- Email addresses: Read-only (without it the app cannot read verified emails and those allowlists deny every GitHub sign-in)
- For existing GitHub Apps, republish the permission change and request/approve installation updates, otherwise the added permission does not apply to current installs.
-
Click "Create GitHub App"
-
Note the App ID (top of page). If enabling GitHub sign-in, also note the Client ID.
-
If enabling GitHub sign-in, under "Client secrets", click "Generate a new client secret" and note the Client Secret.
-
Scroll down to "Private keys" and click "Generate a private key" (downloads a .pem file)
-
Convert the key to PKCS#8 format (required for Cloudflare Workers):
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt \ -in ~/Downloads/your-app-name.*.private-key.pem \ -out private-key-pkcs8.pem
-
Install the app on your account/organization:
- Click "Install App" in the sidebar
- Select the repositories you want Open-Inspect to access
-
Note the Installation ID from the URL after installing:
https://github.com/settings/installations/INSTALLATION_ID
You should now always have:
- App ID (e.g.,
123456) - Private Key (PKCS#8 format, starts with
-----BEGIN PRIVATE KEY-----) - Installation ID (e.g.,
12345678)
For GitHub sign-in, you should also have:
- Client ID (e.g.,
Iv1.abc123...) - Client Secret (e.g.,
abc123...)
Skip this step if you don't need Slack integration.
- Go to Slack API Apps
- Click "Create New App" → "From scratch"
- Name it (e.g.,
Open-Inspect) and select your workspace
After deploying the Slack worker, you can configure the app from
packages/slack-bot/slack-app-manifest.yaml
instead of entering the settings below individually. Replace SLACK_EVENTS_URL with the worker's
/events URL and SLACK_INTERACTIONS_URL with its /interactions URL before applying the
manifest. The template includes message.channels and message.groups for channel-message
automations; remove those subscriptions if the deployment will not use that feature.
Before terraform apply, configure the OAuth scopes below, install the app, and collect its bot
token and signing secret. Apply the URL-dependent manifest after deployment, when Slack can verify
the worker's /events and /interactions endpoints.
- Go to OAuth & Permissions in the sidebar
- Add Bot Token Scopes:
assistant:writeapp_mentions:readchat:writechannels:historychannels:readgroups:historygroups:readim:historyfiles:read(lets the bot read images attached to messages and forward them to sessions)files:writereactions:writeusers:readusers:read.email
- Click "Install to Workspace"
- Note the Bot Token (
xoxb-...)
Important: If you update bot token scopes later, you must reinstall the app to your workspace for the new permissions to take effect.
Queued delivery applies to every Slack completion, including text-only replies. Before the first
terraform apply after upgrading:
- Add Account | Queues | Edit to the Cloudflare API token used by Terraform. Terraform needs this permission to create the completion queue, dead-letter queue, Worker binding, and consumer.
- Ensure the Slack app has
assistant:write,users:read,users:read.email,files:read, andfiles:write. Reinstall the app once for the workspace, and updateslack_bot_tokenif Slack issued a replacement. These scopes enable Agent view, user identity resolution, inbound images, and generated-media delivery. - Run
terraform apply, then verify a text completion, an inbound image attached to a prompt, and a generated-media attachment. If the token lacks Queue access, the apply fails while provisioning the new resources; grant the permission and rerun the apply.
No individual Slack user needs to reauthorize the app. Deployments with enable_slack_bot = false
still create the image-build finalization Queue and dead-letter Queue.
- Go to Basic Information
- Note the Signing Secret
Event Subscriptions require the Slack bot worker to be deployed first for URL verification. You'll configure this in Step 7b after running Terraform.
Skip this step if you don't need the Linear Agent integration.
- Create an application in Linear Settings → API → Applications.
- Enable webhooks and subscribe to Agent session events. Permission changes and Inbox notifications are also useful operational signals.
- Enable Client credentials tokens. This Linear-side setting is not managed by Terraform.
- Configure these URLs, replacing the deployment name and Workers subdomain:
- Callback URL:
https://open-inspect-linear-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/oauth/callback - Webhook URL:
https://open-inspect-linear-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/webhook
- Callback URL:
- Record the client ID, client secret, and webhook signing secret for
terraform.tfvars.
The app is installed after deployment in Step 7d. Runtime access uses replaceable client-credentials tokens; authorization-code refresh tokens are not stored as runtime credentials.
Generate these random secrets (you'll need them for terraform.tfvars):
# Token encryption key
echo "token_encryption_key: $(openssl rand -base64 32)"
# Repo secrets encryption key
echo "repo_secrets_encryption_key: $(openssl rand -base64 32)"
# Modal API secret (use hex for this one)
echo "modal_api_secret: $(openssl rand -hex 32)"
# Browser authentication secret (Terraform retains the legacy input name)
echo "nextauth_secret: $(openssl rand -base64 32)"
# GitHub webhook secret (only if enabling GitHub bot)
echo "github_webhook_secret: $(openssl rand -hex 32)"Save these values somewhere secure—you'll need them in the next step.
cd terraform/environments/production
# Copy the example files
cp terraform.tfvars.example terraform.tfvars
cp backend.tfvars.example backend.tfvarsFill in your R2 credentials:
access_key = "your-r2-access-key-id"
secret_key = "your-r2-secret-access-key"
endpoints = {
s3 = "https://YOUR_CLOUDFLARE_ACCOUNT_ID.r2.cloudflarestorage.com"
}Fill in all the values you gathered. Here's the structure:
# Provider Authentication
cloudflare_api_token = "your-cloudflare-api-token"
cloudflare_account_id = "your-account-id"
cloudflare_worker_subdomain = "your-subdomain" # e.g., "twilight-unit-b2cf" (without .workers.dev)
# Web platform: "vercel" (default) or "cloudflare" (OpenNext)
web_platform = "vercel"
# Optional custom domain for the web app (only when web_platform = "cloudflare")
# cloudflare_zone_id = "your-zone-id"
# cloudflare_custom_domain = "app.example.com"
# Vercel (only required when web_platform = "vercel")
# If using Cloudflare, do NOT set these — leave them out so the dummy defaults are used.
vercel_api_token = "your-vercel-token"
vercel_team_id = "team_xxxxx" # Your Vercel ID (even personal accounts have one)
modal_token_id = "your-modal-token-id"
modal_token_secret = "your-modal-token-secret"
modal_workspace = "your-modal-workspace"
modal_environment = "your-modal-environment"
modal_environment_web_suffix = "your-modal-web-suffix" # Lowercase letters, digits, dashes; empty for https://workspace--... endpoints
# Sandbox provider: "modal" (default), "daytona", or "vercel"
# sandbox_provider = "modal"
# Daytona (only required when sandbox_provider = "daytona")
# daytona_api_url = "https://app.daytona.io/api"
# daytona_api_key = "your-daytona-api-key"
# daytona_base_snapshot = "your-snapshot-name"
# Vercel Sandboxes (only required when sandbox_provider = "vercel")
# vercel_sandbox_token = "your-vercel-token"
# vercel_sandbox_project_id = "prj_xxxxx"
# vercel_sandbox_team_id = "team_xxxxx" # Optional
# vercel_base_snapshot_id = "snapshot_xxxxx" # Optional manual override; skips managed snapshot builds
# vercel_sandbox_runtime = "node24"
# vercel_snapshot_expiration_ms = 0
# E2B (only required when sandbox_provider = "e2b")
# e2b_api_key = "your-e2b-api-key" # runtime REST API key (also auths the build)
# e2b_template_id = "open-inspect-sandbox"
# GitHub App repository access (required in every deployment)
github_app_id = "123456"
github_app_installation_id = "12345678"
github_app_private_key = <<-EOF
-----BEGIN PRIVATE KEY-----
... paste your PKCS#8 key here ...
-----END PRIVATE KEY-----
EOF
# GitHub OAuth sign-in (optional pair; leave both empty for Google-only)
github_client_id = "Iv1.abc123..." # From GitHub App settings
github_client_secret = "your-client-secret" # Generated in GitHub App settings
# Google OAuth sign-in (optional pair; may be used alone or with GitHub)
google_client_id = ""
google_client_secret = ""
# Slack (set enable_slack_bot = false to disable Slack integration)
enable_slack_bot = false
slack_bot_token = ""
slack_signing_secret = ""
# GitHub Bot (set enable_github_bot = true to deploy the webhook worker)
enable_github_bot = false
github_webhook_secret = "" # From Step 5 (required if enabled)
github_bot_username = "" # e.g., "my-app[bot]" (your GitHub App's bot login)
# Linear Agent (set enable_linear_bot = true to deploy the webhook worker)
enable_linear_bot = false
linear_client_id = "" # From Step 4b (required if enabled)
linear_client_secret = "" # From Step 4b (required if enabled)
linear_webhook_secret = "" # From Step 4b (required if enabled)
# API Keys
anthropic_api_key = "sk-ant-..."
# Slack/Linear classifier provider, chosen by classification_model.
# An OpenAI model requires classification_openai_api_key. An Anthropic model
# needs no new value — it is served by anthropic_api_key above.
# classification_model = "claude-haiku-4-5" # e.g. "gpt-5.4-mini" to classify on OpenAI
classification_openai_api_key = "" # Required when classification_model is an OpenAI id
# Security Secrets (from Step 5)
token_encryption_key = "your-generated-value"
repo_secrets_encryption_key = "your-generated-value"
# provider_accounts_encryption_key = "existing-key" # Optional override; Terraform generates one
modal_api_secret = "your-generated-value"
nextauth_secret = "your-generated-value"
# Configuration
# IMPORTANT: deployment_name must be globally unique for Vercel URLs
# Use your GitHub username, company name, or a random string
deployment_name = "your-unique-name" # e.g., "acme", "johndoe", "mycompany"
project_root = "../../../"
# Branding (optional — defaults shown)
# Display name shown in the web UI tab title, sign-in page, landing hero, bot
# messages (Slack/Linear), PR body footer, and outbound HTTP User-Agent.
# app_name = "Open-Inspect"
# Short brand label shown only in the sidebar header.
# Optional URL (absolute or root-relative) to a custom logo/favicon override.
# Leave empty to keep the built-in favicon and default in-app icon.
# app_icon_url = ""
# Initial deployment: set both to false (see Step 7)
enable_durable_object_bindings = false
enable_service_bindings = false
# Access Control (set at least one allowlist for production). A user is admitted
# if they match ANY allowlist below.
allowed_users = "your-github-username" # Comma-separated GitHub usernames, or empty
allowed_email_domains = "" # Comma-separated domains (e.g., "example.com,corp.io")
allowed_emails = "" # Exact addresses (e.g., "pm@gmail.com") — for users on shared domains
allowed_github_orgs = "" # Comma-separated orgs whose active members can sign in
# Explicitly opt into open access only if you want any authenticated user to be
# able to sign in when all allowlists are empty.
unsafe_allow_all_users = falseComplete credential pairs are the enablement policy. Terraform rejects partial pairs and rejects a deployment with no sign-in provider.
| Configuration | GitHub client pair | Google client pair | Compatible admission |
|---|---|---|---|
| GitHub-only | Set | Empty | GitHub username/org, verified email/domain, or unsafe |
| Google-only | Empty | Set | Verified email/domain, or explicit unsafe allow-all |
| GitHub and Google | Set | Set | Verified email/domain, or explicit unsafe allow-all |
The GitHub App ID, PKCS#8 private key, and installation ID remain required in all three
configurations because they authorize repository operations; they do not enable GitHub sign-in. The
/login page reads the enabled provider set from the control plane on every request.
Note: Review
allowed_users,allowed_email_domains,allowed_emails, andallowed_github_orgscarefully — these control who can sign in. Terraform fails if all are empty unless you explicitly setunsafe_allow_all_users = true. Allowlists use OR semantics: matching any configured username, email domain, exact email, or active GitHub org membership grants access. Useallowed_emailsfor individual users on shared domains (e.g. a specificperson@gmail.com) whereallowed_email_domainswould admit too many.allowed_github_orgschecks membership at sign-in only with the signing-in user's OAuth token; existing sessions last until session expiry. Theread:orgOAuth scope is requested only when org access is configured, and GitHub Apps using org access need Organization permissions: Members read-only.
Google login lets non-developer users (PMs, support agents) sign in without a GitHub account. Git operations still use the shared GitHub App, and their PRs fall back to the App bot (no personal GitHub attribution unless the same verified email is also a linked GitHub identity).
- In the Google Cloud Console, create an OAuth client ID of type Web application.
- Add the authorized redirect URI
{your-web-app-url}/api/auth/callback/google(e.g.https://open-inspect-yourname.vercel.app/api/auth/callback/google). It must match the deployed URL exactly. - On the OAuth consent screen, request only the
openid,email, andprofilescopes — these are non-sensitive, so Google requires no app-verification review. - Set
google_client_idandgoogle_client_secret(both required together), and add at least one allowed user toallowed_emails(exact addresses) orallowed_email_domains. Leave the GitHub client pair empty for Google-only sign-in, or keep it configured to offer both providers. The next request to/loginreflects the deployed pair without a web flag or rebuild.
Security note: Google sign-in is admitted only for verified emails that match an allowlist. Because addresses on shared domains like
gmail.comare generic, preferallowed_emails(exact match) overallowed_email_domainsfor those users.
Deployment requires two phases due to Cloudflare's Durable Object and service binding requirements.
Ensure your terraform.tfvars has:
enable_durable_object_bindings = false
enable_service_bindings = falseImportant: Build the workers before running Terraform (Terraform references the built bundles):
# From the repository root
npm run build -w @open-inspect/control-plane -w @open-inspect/slack-bot -w @open-inspect/github-botThen run:
cd terraform/environments/production
# Initialize Terraform with backend config
terraform init -backend-config=backend.tfvars
# Deploy (phase 1 - creates workers without bindings)
terraform applyAfter Phase 1 succeeds, update your terraform.tfvars:
enable_durable_object_bindings = true
enable_service_bindings = trueThen run:
terraform applyTerraform will update the workers with the required bindings.
Owner assignment is an explicit operator action. After both deployment phases complete:
- Have the intended Owner sign in to the deployed web application once. This creates their canonical user and default role assignment.
- While signed in, open
/api/auth/get-sessionon the web application origin and record the 32-character lowercase hexadecimaluser.id. The bootstrap command accepts this canonical ID, never an email address. - Obtain the D1 database name with
terraform output -raw d1_database_namefromterraform/environments/production. - From the repository root, run the remote dry run (the default):
npm run rbac:bootstrap-owner -- \
--database "$(terraform -chdir=terraform/environments/production output -raw d1_database_name)" \
--user "<canonical-user-id>"- Confirm the preflight result is
ready(orno-opwhen the target is already the current unsuspended Owner), then execute the same command with--execute:
npm run rbac:bootstrap-owner -- \
--database "$(terraform -chdir=terraform/environments/production output -raw d1_database_name)" \
--user "<canonical-user-id>" \
--executeThe command uses Wrangler credentials (CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID, or
wrangler login) and targets remote D1. It refuses a suspended/missing user, a missing or ambiguous
assignment, or another unsuspended Owner. There is no force option. Execution is one atomic Wrangler
SQL file: it writes one redacted workspace.owner_bootstrapped service audit event and replaces the
target's assignment. A no-op writes nothing.
- Verify the control-plane health response contains
"rbac":{"ownerAssignment":"present"}:
curl "$(terraform -chdir=terraform/environments/production output -raw control_plane_url)/health"This health value reports current state: present means at least one Owner assignment belongs to an
unsuspended user.
Now that the Slack bot worker is deployed, configure the agent experience, App Home, and event subscriptions.
- Go to Slack Apps -> Your Slack App → Agents
- Enable the agent feature and use
AI coding assistant for your codebaseas the agent description
The App Home provides settings for users' preferred model, reasoning effort, and branch. The writable Messages tab lets users start direct-message sessions.
- Go to Slack Apps -> Your Slack App → App Home
- Under Show Tabs, toggle "Home Tab" to On
- Toggle "Messages Tab" to On and allow users to send messages
- Go to Slack Apps -> Your Slack App → Event Subscriptions
- Toggle "Enable Events" to On
- Enter the Request URL shown by
terraform output -raw slack_bot_events_url:(Replacehttps://open-inspect-slack-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/eventsYOUR-SUBDOMAINwith your Cloudflare Workers subdomain and{deployment_name}with your deployment name from terraform.tfvars) - Wait for the green "Verified" checkmark
- Under Subscribe to bot events, add:
app_home_opened(required for App Home settings)app_mentionmessage.channels(optional - if you want the bot to see all channel messages)message.groups(optional - if you want automations in private channels)message.im(enables direct message support)
- Click Save Changes
- Go to Interactivity & Shortcuts
- Toggle "Interactivity" to On
- Enter the Request URL shown by
terraform output -raw slack_bot_interactions_url:https://open-inspect-slack-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/interactions - Under Select Menus, enter Options Load URL using the same endpoint:
This is required for searchable Slack repository pickers that use external data sources.
https://open-inspect-slack-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/interactions - Click Save Changes
In Slack, for each channel where you want the bot to respond:
- Type
/invite @YourBotName, or - Click the channel name → Integrations → Add apps
The bot only responds to @mentions in channels it has been invited to.
Now that the GitHub bot worker is deployed, configure the GitHub App for webhook delivery.
- Go to your GitHub App settings
- Select your Open-Inspect app
- Under Webhook:
- Check "Active"
- Webhook URL:
(Replace
https://open-inspect-github-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/webhooks/githubYOUR-SUBDOMAINwith your Cloudflare Workers subdomain and{deployment_name}with your deployment name from terraform.tfvars) - Webhook secret: Enter the
github_webhook_secretvalue from your terraform.tfvars
- Under Subscribe to events, check:
- Pull requests
- Issues
- Issue comments
- Pull request reviews
- Pull request review comments
- Check suites
- Workflow runs (required for GitHub workflow-run automations)
- Click Save changes
Your GitHub App's bot username is its slug with [bot] appended. You can find it by:
- Having the bot perform any action (e.g., a PR review)
- Checking the actor's login in the webhook payload
Or construct it from your App's slug: if your app is named My-Inspect-App, the bot username is
my-inspect-app[bot]. Ensure this matches the github_bot_username value in your terraform.tfvars.
- Code Review: Open a non-draft PR in a repository where auto-review is enabled — it performs an automated review
- Comment Actions: @mention the bot in a PR comment with instructions (e.g.,
@my-app[bot] explain why this test is failing)
For day-to-day workflows, see GitHub Integration.
After the Linear bot Worker is deployed, visit:
https://open-inspect-linear-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/oauth/authorize
A Linear workspace admin must approve the installation. After installation, the agent appears in mention and assignment menus. Test it by mentioning the agent on an issue, then use View Session to follow the corresponding Open-Inspect session.
For upgrades, enable Client credentials tokens before deploying. No reinstall is expected for an eligible existing installation, but allow already-running sessions to finish before upgrading because older callback contexts may not contain the installed app-user identity.
For configuration and troubleshooting, see Linear Integration.
Terraform handles the full build and deploy automatically — the web app is built with OpenNext and
deployed as a Cloudflare Worker during terraform apply. No manual step needed.
By default the web app is served from
https://open-inspect-web-{deployment_name}.YOUR-SUBDOMAIN.workers.dev. To use your own hostname,
set both of these in terraform.tfvars:
cloudflare_zone_id = "your-zone-id" # zone that owns the hostname
cloudflare_custom_domain = "app.example.com" # bare hostname, no schemeCloudflare provisions the DNS record and edge certificate automatically. Notes:
- The canonical browser-auth origin and the links the bots send become
https://{your-custom-domain}, and the workers.dev route for the web Worker is disabled so the app has a single canonical origin. - Update the GitHub App callback URL (and the Google redirect URI, if Google login is enabled) to the new hostname, or sign-in will fail with a redirect URI mismatch.
- The Cloudflare API token needs zone-level Workers Routes: Edit permission to attach the domain.
Terraform creates the Vercel project and configures environment variables, but does not deploy the code. You have two options:
# From the repository root (replace {deployment_name} with your value from terraform.tfvars)
npx vercel link --project open-inspect-{deployment_name}
npx vercel --prodNote: The Vercel project is configured with custom build commands for the monorepo structure. Terraform sets these automatically:
- Install:
cd ../.. && npm install && npm run build -w @open-inspect/shared- Build:
next build
- Go to Vercel Dashboard
- Find the
open-inspect-{deployment_name}project - Go to Settings → Git
- Click "Connect Git Repository" and select your fork
- Vercel will automatically deploy on push to main
Note: If you link Git, ensure the build settings match those configured by Terraform (Settings → General → Build & Development Settings).
After deployment completes, verify each component:
# Get the verification commands from Terraform
terraform output verification_commandsOr manually:
# 1. Control Plane health check (replace {deployment_name} and YOUR-SUBDOMAIN)
curl https://open-inspect-control-plane-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/health
# 2. Sandbox backend health check
# Modal exposes a health endpoint. Prefer the exact URL from terraform output verification_commands.
# Manual form: https://<workspace>[-<modal_environment_web_suffix>]--open-inspect-api-health.modal.run
MODAL_WORKSPACE_SLUG="YOUR-WORKSPACE" # or "YOUR-WORKSPACE-YOUR-MODAL-WEB-SUFFIX"
curl https://${MODAL_WORKSPACE_SLUG}--open-inspect-api-health.modal.run
# Daytona and Vercel use their provider APIs directly, so there is no Open-Inspect shim health URL.
# 3. Web app (should return 200)
curl -I "$(terraform output -raw web_app_url)"- Visit your web app URL
- Sign in with each configured provider
- Create a new session with a repository
- Send a prompt and verify the sandbox starts
Enable automatic deployments when you push to main by adding GitHub Secrets.
Go to your fork's Settings → Secrets and variables → Actions, and add:
| Secret Name | Value |
|---|---|
CLOUDFLARE_API_TOKEN |
Your Cloudflare API token |
CLOUDFLARE_ACCOUNT_ID |
Your Cloudflare account ID |
CLOUDFLARE_WORKER_SUBDOMAIN |
Your workers.dev subdomain |
DEPLOYMENT_NAME |
Your deployment name |
R2_ACCESS_KEY_ID |
R2 access key ID |
R2_SECRET_ACCESS_KEY |
R2 secret access key |
WEB_PLATFORM |
vercel or cloudflare |
VERCEL_API_TOKEN |
Vercel API token (only if web_platform = "vercel") |
VERCEL_TEAM_ID |
Vercel team/account ID (only if web_platform = "vercel") |
VERCEL_PROJECT_ID |
Vercel project ID (only if web_platform = "vercel") |
MODAL_TOKEN_ID |
Modal token ID |
MODAL_TOKEN_SECRET |
Modal token secret |
MODAL_WORKSPACE |
Modal workspace name |
MODAL_ENVIRONMENT |
Modal environment name (defaults to main) |
MODAL_ENVIRONMENT_WEB_SUFFIX |
Modal environment web suffix for endpoint URLs; lowercase letters, digits, dashes, or empty |
SANDBOX_PROVIDER |
modal, daytona, or vercel |
DAYTONA_API_URL |
Daytona API URL (only if sandbox_provider = "daytona") |
DAYTONA_API_KEY |
Daytona API key (only if sandbox_provider = "daytona") |
DAYTONA_BASE_SNAPSHOT |
Daytona base snapshot name (only if sandbox_provider = "daytona") |
DAYTONA_TARGET |
Optional Daytona target name |
VERCEL_SANDBOX_TOKEN |
Vercel API token (only if sandbox_provider = "vercel") |
VERCEL_SANDBOX_PROJECT_ID |
Vercel project ID for sandbox sessions (only if sandbox_provider = "vercel") |
VERCEL_SANDBOX_TEAM_ID |
Optional Vercel team/account ID for sandbox sessions |
VERCEL_BASE_SNAPSHOT_ID |
Optional manual Vercel base-runtime snapshot; skips Terraform-managed snapshot builds |
VERCEL_SANDBOX_RUNTIME |
Optional Vercel Sandbox runtime (defaults to node24) |
VERCEL_SNAPSHOT_EXPIRATION_MS |
Optional Vercel runtime snapshot expiration in milliseconds (0 means no expiration) |
VERCEL_SANDBOX_API_BASE_URL |
Optional advanced Vercel Sandbox API base URL override |
GH_OAUTH_CLIENT_ID |
Optional GitHub sign-in client ID; set with GH_OAUTH_CLIENT_SECRET |
GH_OAUTH_CLIENT_SECRET |
Optional GitHub sign-in client secret; set with GH_OAUTH_CLIENT_ID |
GOOGLE_CLIENT_ID |
Optional Google sign-in client ID; set with GOOGLE_CLIENT_SECRET |
GOOGLE_CLIENT_SECRET |
Optional Google sign-in client secret; set with GOOGLE_CLIENT_ID |
GH_APP_ID |
Required GitHub App repository-access ID |
GH_APP_PRIVATE_KEY |
Required GitHub App repository-access private key (PKCS#8 format) |
GH_APP_INSTALLATION_ID |
Required GitHub App repository-access installation ID |
ENABLE_SLACK_BOT |
true to deploy Slack bot, false to skip (default: true) |
SLACK_BOT_TOKEN |
Slack bot token (required if enabled) |
SLACK_SIGNING_SECRET |
Slack signing secret (required if enabled) |
ENABLE_LINEAR_BOT |
true to deploy Linear bot, false to skip (default: false) |
LINEAR_CLIENT_ID |
Linear OAuth application client ID (required if Linear enabled) |
LINEAR_CLIENT_SECRET |
Linear OAuth application client secret (required if Linear enabled) |
LINEAR_WEBHOOK_SECRET |
Linear webhook signing secret (required if Linear enabled) |
ANTHROPIC_API_KEY |
Anthropic API key |
CLASSIFICATION_OPENAI_API_KEY |
Classifier OpenAI key (required when classification_model is an OpenAI id) |
OPENAI_API_KEY |
Optional OpenAI API key used when a session selects API-key authentication |
XAI_API_KEY |
Optional xAI API key used when a session selects API-key authentication |
DEEPSEEK_API_KEY |
DeepSeek API key (optional, required only for DeepSeek models) |
TOKEN_ENCRYPTION_KEY |
Generated encryption key (OAuth tokens) |
REPO_SECRETS_ENCRYPTION_KEY |
Generated encryption key (repo secrets) |
PROVIDER_ACCOUNTS_ENCRYPTION_KEY |
Optional existing provider-account key override; Terraform generates one when omitted |
MODAL_API_SECRET |
Generated Modal API secret |
NEXTAUTH_SECRET |
Generated browser-auth secret (legacy Actions secret name) |
ALLOWED_USERS |
Comma-separated GitHub usernames (or empty for all users) |
ALLOWED_EMAIL_DOMAINS |
Comma-separated email domains (or empty for all domains) |
ALLOWED_EMAILS |
Comma-separated exact email addresses (for individual users on shared domains) |
ALLOWED_GITHUB_ORGS |
Comma-separated GitHub orgs whose active members can sign in |
ENABLE_DURABLE_OBJECT_BINDINGS |
Optional Terraform CI flag for Durable Object phase 1 (defaults to true) |
ENABLE_GITHUB_BOT |
true to deploy GitHub bot worker (or empty to skip) |
GH_WEBHOOK_SECRET |
GitHub webhook secret (required if GitHub bot enabled) |
GH_BOT_USERNAME |
GitHub App bot username, e.g., my-app[bot] (required if GitHub bot enabled) |
APP_NAME |
Optional display name for whitelabeling (default: Open-Inspect) |
APP_ICON_URL |
Optional URL to a custom logo/favicon (default: built-in icon) |
CLASSIFICATION_MODEL is an optional Actions variable, not a secret — add it under Settings →
Secrets and variables → Actions → Variables to point the Slack/Linear classifiers at a different
model (for example gpt-5.4-mini). Leave it unset to keep the Terraform default. An OpenAI value
also requires the CLASSIFICATION_OPENAI_API_KEY secret; an Anthropic value is served by
ANTHROPIC_API_KEY.
When enabling or upgrading the Linear bot, also enable Client credentials tokens on the OAuth application in Linear Settings → API → Applications. This provider-side setting is not managed by Terraform. Existing eligible single-workspace installations transition on their next request without uninstalling or reinstalling the app.
Bulk upload secrets with gh CLI:
Instead of adding secrets one by one, create a .secrets file (don't commit this!):
CLOUDFLARE_API_TOKEN=your-token
CLOUDFLARE_ACCOUNT_ID=your-account-id
ANTHROPIC_API_KEY=sk-ant-...
DEEPSEEK_API_KEY=sk-...
# ... add all secrets
Then upload all at once (run from your fork's directory, or use
-R {your_github_username}/{background-agents}):
gh secret set -f .secretsIf you bulk upload from a file, set multiline secrets like GH_APP_PRIVATE_KEY separately so the
PEM formatting is preserved:
gh secret set GH_APP_PRIVATE_KEY < private-key-pkcs8.pemOnce configured, the GitHub Actions workflow will:
- Run
terraform planon pull requests (with PR comment) - Run
terraform applywhen merged to main
Terraform generates and persists PROVIDER_ACCOUNTS_ENCRYPTION_KEY when no override is configured.
Existing local Terraform installations retain an existing key through the
provider_accounts_encryption_key input; Actions deployments retain it through the
PROVIDER_ACCOUNTS_ENCRYPTION_KEY repository or production-environment secret. Changing the key
makes stored provider credentials unreadable. Preserve backups of the remote Terraform state because
it is the recovery source for automatically generated keys.
To update after pulling changes from upstream:
Terraform generates the provider-account credential key for installations without an existing
override. For local applies, keep any existing provider_accounts_encryption_key input unchanged.
For Actions deployments, keep any existing PROVIDER_ACCOUNTS_ENCRYPTION_KEY repository or
production-environment secret unchanged so stored provider credentials remain readable.
# Pull latest changes
git pull upstream main
# Rebuild shared package if it changed
npm run build -w @open-inspect/shared
# Re-run Terraform (it only changes what's needed)
cd terraform/environments/production
terraform applyOpen Settings > Provider Accounts to add and verify accounts. Setting a provider default changes only sessions created afterward; existing sessions remain pinned to legacy scoped OAuth, a specific account, or API-key mode. Legacy credentials may coexist during rollout, and the settings page lists their locations. Remove them only after dependent legacy-bound sessions are no longer needed. Rebuild all sandbox runtime images, templates, and provider snapshots so new sessions use the generic broker.
Re-run init with backend config:
terraform init -backend-config=backend.tfvars- Verify the private key is in PKCS#8 format (starts with
-----BEGIN PRIVATE KEY-----) - Check the Installation ID matches your installation
- Ensure the app has required permissions on the repository
- Verify the callback URL matches your deployed web app URL exactly
The callback URL in your GitHub App settings doesn't match your deployed URL. Update the callback URL to match your web app URL:
- Vercel:
https://open-inspect-{deployment_name}.vercel.app/api/auth/callback/github - Cloudflare:
https://open-inspect-web-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/api/auth/callback/github - Cloudflare with a custom domain:
https://{your-custom-domain}/api/auth/callback/github
# Check Modal CLI is working (from packages/modal-infra)
cd packages/modal-infra
uv run modal token show
# View Modal logs
uv run modal app logs open-inspectThe sandbox_runtime package is a sibling package that must be installed before deploying. From the
repository root:
cd packages/modal-infra && uv sync --frozen && cd -This installs all Modal deployment dependencies including sandbox_runtime (resolved via
[tool.uv.sources] in pyproject.toml).
Terraform references the built worker bundles. Build them before running terraform apply:
# Build shared package first
npm run build -w @open-inspect/shared
# Build workers (required before Terraform)
npm run build -w @open-inspect/control-plane -w @open-inspect/slack-bot -w @open-inspect/github-bot -w @open-inspect/linear-bot
# Verify bundles exist
ls packages/control-plane/dist/index.js
ls packages/slack-bot/dist/index.js
ls packages/github-bot/dist/index.js # Only if enable_github_bot = true
ls packages/linear-bot/dist/index.js # Only if enable_linear_bot = true- Verify Event Subscriptions URL is verified (green checkmark)
- Ensure the bot is invited to the channel (
/invite @BotName) - Check that you're @mentioning the bot in your message
- If you updated bot token scopes, reinstall the app to your workspace
If the bot doesn't see the original message when tagged in a thread reply:
- Verify the bot has
channels:historyscope (for public channels) andgroups:history(for private channels). These are required by theconversations.repliesAPI to fetch thread messages. - Verify the bot has
channels:readandgroups:readscopes. These are required byconversations.infoto fetch channel name and description for context, and byconversations.listto populate the automation channel picker. If the picker shows no channels, check these scopes and that the bot is invited to the target channel. - If you added missing scopes, reinstall the app to your workspace for the new permissions to take effect.
- Verify the bot has the
files:readscope and reinstall the app after adding it. Thefiles:writescope is for generated media posted back to Slack, not images sent to the agent. - Use PNG, JPEG, WebP, or GIF images no larger than 10 MiB. Open-Inspect forwards at most six images per message.
- In a channel,
@mentionthe bot with the image. DMs do not require a mention. Watched-channel automations do not forward file attachments. - For channel mentions and replies, verify
channels:historyfor public channels orgroups:historyfor private channels. Slack may omit files from the mention event, so the bot uses conversation history to retrieve them. - Check the Slack thread for a warning about images that were too large or could not be downloaded or uploaded. Other images and any text are still sent when possible.
- Verify the bot has the
files:writescope and reinstall the app after adding it. - Confirm the agent registered the image or video as a session artifact; repository files are not uploaded automatically.
- Check that the file is PNG, JPEG, WebP, or MP4 and no larger than 10 MiB. A completion attaches at most five files and 25 MiB total; other media remains available through View Session.
- Check Slack workspace policies for disabled uploads, prohibited file types, or exhausted storage.
- Verify the webhook URL matches
https://open-inspect-github-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/webhooks/github - Check the webhook secret matches
github_webhook_secretin terraform.tfvars - Confirm
enable_github_bot = truein terraform.tfvars and the worker is deployed - Check that
github_bot_usernamematches your App's bot login (e.g.,my-app[bot]) - For PR reviews, ensure auto-review is enabled for the repository and the PR is not a draft
- For comment actions, ensure the bot is @mentioned in a PR comment (not an issue)
If sessions fail with "Model not found" when using sandbox_provider = "daytona" or
sandbox_provider = "vercel", the required LLM API key is likely missing. Unlike Modal (which
injects keys automatically), these providers require you to add them as global secrets:
- Go to Settings > Secrets in the web app
- Select All Repositories (Global) from the scope dropdown
- Add the key for your chosen provider (e.g.,
ANTHROPIC_API_KEYfor Claude models orDEEPSEEK_API_KEYfor DeepSeek models, orZHIPU_API_KEYfor Z.AI Coding Plan models) - Click Save
See Secrets Management for more on global and repository secrets.
The Vercel Terraform provider validates its API token on initialization, even when no Vercel
resources are created. If you set vercel_api_token = "" in your terraform.tfvars, the provider
will reject it. Fix: Remove the vercel_api_token and vercel_team_id lines from your
terraform.tfvars entirely — the built-in defaults ("unused") satisfy the provider's non-empty
validation. This is a known Terraform limitation (providers validate credentials on init regardless
of whether any resources use them).
This occurs on first deployment. Follow the two-phase deployment process:
- Deploy with
enable_durable_object_bindings = falseandenable_service_bindings = false - After success, set both to
trueand runterraform applyagain
- Never commit
terraform.tfvarsorbackend.tfvarsto source control - The
.gitignorealready excludes these files - Use GitHub Secrets for CI/CD, not hardcoded values
- Rotate secrets periodically using
terraform applyafter updatingterraform.tfvars - Review the Security Model - this system is designed for single-tenant deployment
Open-Inspect can be whitelabeled by overriding the brand name and logo. Both values are optional and
default to the built-in Open-Inspect brand.
Add these to your terraform.tfvars:
# Display name shown in:
# - Web tab title, sign-in page, landing hero
# - Slack App Home settings page
# - Linear OAuth success page and completion comments
# - PR body footer ("Created with [<app_name>](<session-url>)")
# - Outbound HTTP User-Agent headers (GitHub, GitLab API)
app_name = "Acme Bot"
# Optional URL to a custom logo image (SVG/PNG). When set, replaces the icon in
# the command menu and favicon. Leave empty to keep the built-in favicon.
# Use an absolute URL or a root-relative path served from packages/web/public/.
app_icon_url = "/branding/acme-logo.svg" # or "https://cdn.example.com/logo.svg"After changing any of these values, run terraform apply and (for Vercel) redeploy the web app so
the new build picks up the NEXT_PUBLIC_APP_NAME and NEXT_PUBLIC_APP_ICON_URL env vars
(Cloudflare's web deploy is rebuilt automatically by Terraform).
Note:
NEXT_PUBLIC_*vars are inlined into the client bundle at build time, so changes require a fresh web build. The bot/control-plane workers readAPP_NAMEat request time, so they pick up the new value immediately afterterraform apply.
For details on the infrastructure components, see:
- terraform/README.md - Terraform module documentation
- README.md - System architecture overview
- AVAILABLE_MODELS.md - Supported model list and reasoning efforts
- OPENAI_MODELS.md - Configuring OpenAI Codex models
- GROK_MODELS.md - Configuring Grok with a SuperGrok subscription