# Companion v2 local environment. # --- Public origins and product gates -------------------------------------- COMPANION_API_URL=http://127.0.0.1:3001 COMPANION_WEB_URL=http://127.0.0.1:3000 # Health `release_id` is derived automatically from the deployed commit: Railway injects # RAILWAY_GIT_COMMIT_SHA (preferred), then SOURCE_COMMIT, then GITHUB_SHA in CI. Local/dev falls # back to "local". Do not set COMPANION_RELEASE_ID; leftovers are ignored. NEXT_PUBLIC_COMPANION_API_URL=http://127.0.0.1:3001 # Optional PostHog web analytics. Get these from the PostHog project settings. NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN= NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com # Optional Companions control plane. Set the same server-only flag and allowlist on web, API, # worker, and runtime. Restart all four after changing them. The worker reads them only to decide # whether Companion routines may enqueue turns; it never holds a Box credential. Unset/false is the # operational kill switch: already-accepted intent stays durable while runtime stops claims and # interrupts active work safely, and no routine fires. COMPANION_COMPANIONS_ENABLED=false # Required when the master flag is true: comma-separated, case-insensitive email domains. Only # authenticated users with an exact matching domain may see or use Companions. Unset/empty keeps # Companions disabled and routes unregistered; missing or malformed user emails are denied. COMPANION_COMPANIONS_ALLOWED_EMAIL_DOMAINS= # --- Dedicated Companions runtime ------------------------------------------ # API-only address for its HMAC-authenticated desktop request to apps/runtime. Keep this endpoint # private. Railway should use http://${{runtime.RAILWAY_PRIVATE_DOMAIN}}:${{runtime.PORT}} and must # not generate a public domain for runtime. COMPANION_RUNTIME_PRIVATE_URL=http://127.0.0.1:3007 # API + runtime only. Generate independently from COMPANION_SECRETS_MASTER_KEY: # node -e "process.stdout.write(require('crypto').randomBytes(32).toString('base64'))" COMPANION_RUNTIME_DESKTOP_HMAC_SECRET= # Local bind. On Railway omit COMPANION_RUNTIME_PORT, set the runtime service variable PORT=3007, # bind COMPANION_RUNTIME_HOST=:: for IPv4/IPv6 private networking, and expose no public route. COMPANION_RUNTIME_HOST=127.0.0.1 COMPANION_RUNTIME_PORT=3007 COMPANION_RUNTIME_CONCURRENCY=8 COMPANION_RUNTIME_SWEEP_INTERVAL_MS=2000 # Optional per-process UUID. Leave unset so each runtime replica generates a unique executor id; # never share one configured value across replicas. # COMPANION_RUNTIME_EXECUTOR_ID= # Must remain shorter than the fixed 30-second lease and Railway's 30-second drain window. COMPANION_RUNTIME_SHUTDOWN_DRAIN_MS=25000 # Require a baked runtime image before a Box create: when true, a create that cannot resolve a # snapshot fails closed instead of falling back to a full boot-time install. Empty/false keeps the # fallback. Left empty by default. COMPANION_RUNTIME_REQUIRE_IMAGE= # Runtime-only Box/Pi settings. Never provide these to API, worker, or web. The key is required only # when Companions is effectively enabled; a disabled runtime still starts for health and gate control. # Conductor reads this value from the workspace's repo-root .env before Run starts. Its local launcher # also normalizes ascii.dev's BOX_API_KEY spelling, but production deployments should use this # canonical runtime-only name. COMPANION_BOX_API_KEY= COMPANION_BOX_API_BASE=https://ascii.dev/api/box/v1 COMPANION_BOX_ENVIRONMENT= # Refreshed after every message Pi accepts, so idle expiry is six hours after the last message. COMPANION_BOX_TTL_SECONDS=21600 # Optional Box provisioning timings. Unset falls back to these same in-code defaults. COMPANION_BOX_POLL_INTERVAL_MS=1000 COMPANION_BOX_READY_TIMEOUT_MS=120000 # Runtime maintenance command only. Provider deletion remains fail-closed after this bounded wait. COMPANION_BOX_DELETE_POLL_INTERVAL_MS=1000 COMPANION_BOX_DELETE_TIMEOUT_MS=900000 # How long one desktop join waits for Box to finish provisioning a VNC stream before it falls back to # the WebRTC desktop. Every join mints a fresh URL, so this is a per-join wait, not a stream lifetime. COMPANION_BOX_DESKTOP_MINT_BUDGET_MS=15000 # How long a restarted Pi daemon has to expose its ready broker socket before a wake fails. The # bounded wait stays inside the activation Box command so no concurrent provider polling slows Pi. COMPANION_PI_DAEMON_ACTIVE_TIMEOUT_MS=180000 # Prefer a Box template with pinned Pi. Otherwise set an operator-controlled pinned install command. COMPANION_PI_INSTALL_COMMAND= # Set to true to lay Boxes out from the self-hosted Pi bundle instead of installing Pi from npm at # boot. The pinned tarball (packages/box-runtime/src/piBundle.ts) lives in the existing # skill-archives bucket under the pi-bundles/ prefix — no extra bucket, never public — and the # runtime mints a short-lived presigned GET URL from its own S3_* credentials for each layout script # it stages. Anything but true, or an incomplete S3 configuration, keeps the # COMPANION_PI_INSTALL_COMMAND escape hatch. When both are set, the bundle wins. Only the pinned # checksum, never the URL, is trusted. COMPANION_PI_BUNDLE_ENABLED= # Pin the Pi extension used for labeled MCP account injection. COMPANION_PI_MCP_ADAPTER_PACKAGE=npm:pi-mcp-adapter@2.12.1 # Phase 2 direct-transport rollout gate: off | shadow | on. Empty means off. `shadow` and `on` # register the on-box Companion agent's hosted proxy endpoint at every staging (`host ` via # the provider) and store it encrypted. `on` routes the event path (broker state, event long-poll # reads/acks, daemon probe) over that channel with per-call fallback to the exec transport; # `shadow` only logs a throttled direct-vs-exec comparison and a registration failure never fails # a wake, while in `on` registration fails closed. Any other value refuses to boot. COMPANION_DIRECT_TRANSPORT= # Web access, subagent delegation, and memory are installed on every Box from pins held in # packages/box-runtime; they are what a Companion is, not a deployment setting, so there is no # variable for them here. Changing a pin there changes the Box layout marker, and every existing Box # reinstalls the new set the next time a member sends it a message. # Model-provider keys (for example ZAI_API_KEY) are intentionally not inherited from this file by # application processes. Connect them through Companions > Providers so the API can envelope-encrypt # them and the runtime can redeem only the selected Companion's credential. # Local macOS development only: the Conductor "iOS (live Box)" action reads this launcher-owned # value once, connects z.ai through the ordinary /v1 provider route, then relies on the encrypted # database connection just like production. dev-process.sh removes it from API, worker, runtime, # web, and simulator environments. It is never needed in a deployment. COMPANION_IOS_LOCAL_ZAI_API_KEY= # API only. A non-empty key enables native Gemini Live voice transcription for every workspace and # exposes the composer microphone to every Owner/Editor; when empty, clients hide the control. The # API exchanges it for single-use tokens and never returns this long-lived key or receives audio. COMPANION_GEMINI_TRANSCRIPTION_API_KEY= # GitHub's hosted MCP remote uses a deployment-owned GitHub OAuth App. API needs these values for # connect/reconnect and runtime needs them to refresh an expiring encrypted account before staging; # worker, web, and the Box never receive them. Register this exact callback: # ${COMPANION_WEB_URL}/v1/companion-plugins/oauth/callback # Linear and Notion use MCP dynamic client registration and need no deployment-owned client keys. COMPANION_MCP_GITHUB_CLIENT_ID= COMPANION_MCP_GITHUB_CLIENT_SECRET= # Slack uses a deployment-owned Slack app and Bot User OAuth. Only API receives these values; # runtime, worker, web, Pi, and Box do not. Register the same callback URL shown above. COMPANION_MCP_SLACK_CLIENT_ID= COMPANION_MCP_SLACK_CLIENT_SECRET= # Gmail uses a separate Google web OAuth client so restricted mailbox consent is never coupled to # Better Auth sign-in. Enable gmail.googleapis.com and gmailmcp.googleapis.com, register the same # Companion plugin callback above, and request gmail.readonly plus gmail.compose. The runtime # allow-lists read/search/draft tools and never exposes send in v1. COMPANION_MCP_GMAIL_CLIENT_ID= COMPANION_MCP_GMAIL_CLIENT_SECRET= # In production set BETTER_AUTH_URL to the API's public origin — the Google OAuth callback URL is # derived from it (${BETTER_AUTH_URL}/auth/callback/google) and must be registered with Google. BETTER_AUTH_URL=http://127.0.0.1:3001 BETTER_AUTH_COOKIE_PREFIX=better-auth BETTER_AUTH_SECRET=replace-with-a-32-byte-random-secret # Required by Secrets in production. API encrypts and runtime decrypts only currently authorized # Companion provider/MCP material; do not expose it to web or worker. Generate with: # node -e "process.stdout.write(require('crypto').randomBytes(32).toString('base64'))" # Local launchers generate and persist a gitignored workspace key when this is empty. COMPANION_SECRETS_MASTER_KEY= # --- iOS Companion notifications (optional, worker only) ------------------- # A complete Apple token-authentication tuple enables APNs. With all three empty the supervisor is # disabled; a partial tuple fails only that optional supervisor. Encode the .p8 file bytes as base64. COMPANION_APNS_KEY_ID= COMPANION_APNS_TEAM_ID= COMPANION_APNS_PRIVATE_KEY_BASE64= # --- GitHub skill mirrors (optional GitHub App) ----------------------------- # Register one GitHub App with user-to-server OAuth enabled. Callback URL: # ${COMPANION_WEB_URL}/v1/integrations/github/callback (the web origin proxies /v1 to the API) # Permissions: Metadata read, Contents read/write, Administration read/write. # In split production deployments, expose slug/client credentials only to the API and App ID/private # key only to the worker. Local launchers read this combined file once, then dev-process.sh applies # the same per-process allowlists before starting API, worker, runtime, or web. GITHUB_APP_ID= GITHUB_APP_SLUG= GITHUB_APP_CLIENT_ID= GITHUB_APP_CLIENT_SECRET= # PEM with literal \n characters or the PEM bytes encoded as base64. GITHUB_APP_PRIVATE_KEY= GITHUB_APP_NAME= # API-side acknowledgement that a separately configured worker can converge mirrors. # Leave false to disable Workspace Settings > GitHub cleanly. COMPANION_GITHUB_SYNC_ENABLED=false # SaaS only: renders the official Companion App wording. Self-hosted stays false. COMPANION_GITHUB_APP_MANAGED=false # --- Skill databases -------------------------------------------------------- COMPANION_SKILL_DATABASES_ENABLED=false # Bound hot-realm lock retries and queued SQLite WASM work. COMPANION_SKILL_DB_LOCK_WAIT_MS=2500 COMPANION_SKILL_DB_STORAGE_TIMEOUT_MS=5000 COMPANION_SKILL_DB_DELETE_TIMEOUT_MS=5000 COMPANION_SKILL_DB_MAX_QUEUED_TASKS=8 COMPANION_SKILL_DB_MAX_QUEUED_BYTES=67108864 COMPANION_SKILL_DB_MAX_STARTUP_FAILURES=3 COMPANION_SKILL_DB_STARTUP_TIMEOUT_MS=10000 # --- Google OAuth (Better Auth social provider) ----------------------------- # Optional: the "Continue with Google" button always renders, but Google sign-in is only wired when # both are set. Authorized redirect URI to register in Google Cloud: ${BETTER_AUTH_URL}/auth/callback/google GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= # --- Postgres --------------------------------------------------------------- # API, worker, and runtime URLs must use three distinct LOGIN NOSUPERUSER/NOBYPASSRLS/NOINHERIT # roles that do not own tables. Local launchers consume the three named URLs; in production each # service receives only its own connection variable. Migrations use the separate owner URL; see # packages/db/runtime-role-grants.sql. DATABASE_URL=postgres://companion_api:companion-api@127.0.0.1:5432/companion DATABASE_WORKER_URL=postgres://companion_worker:companion-worker@127.0.0.1:5432/companion DATABASE_COMPANION_RUNTIME_URL=postgres://companion_runtime_v2:companion-runtime-v2@127.0.0.1:5432/companion DATABASE_MIGRATION_URL=postgres://companion:companion@127.0.0.1:5432/companion # Production one-shot release job: checkpoint through 0092, apply the exact separated grants, then # admit the guarded 0093 cutover on that same PostgreSQL connection. Never provide these variables # or DATABASE_MIGRATION_URL to a long-lived API, worker, runtime, or web process. DATABASE_API_ROLE=companion_api DATABASE_WORKER_ROLE=companion_worker # It receives only the narrow Runtime v2 SECURITY DEFINER functions and no direct runtime-table # privileges. apps/runtime verifies that its login is isolated before it starts. DATABASE_COMPANION_RUNTIME_ROLE=companion_runtime_v2 # Upgrade only: after ALTER ROLE ... NOLOGIN, draining every old session, and removing its role # memberships, name the former union credential so the hook can revoke all remaining ACLs. # Fresh installs omit this only when no legacy union ACL footprint exists. DATABASE_RUNTIME_ROLE is # permanently rejected as an active credential. # DATABASE_RETIRED_RUNTIME_ROLE=companion_runtime_legacy # Release job only: optional readable override for packages/db/runtime-role-grants.sql. # Normal source and packaged images discover the file without this setting. # COMPANION_RUNTIME_GRANTS_FILE= # Deployments upgrading from a release that still ran Projects and runs must settle their object # storage and sandbox obligations once, with `node dist/cutover.js`, before migration 0063 can apply. # It needs the migration URL above and the S3 credentials below. See deploy/railway/README.md. COMPOSE_BIND_HOST=127.0.0.1 POSTGRES_PORT=5432 # --- Sentry monitoring ------------------------------------------------------ # Optional Sentry DSNs for error tracking and performance monitoring. # Frontend DSN is inlined into the Next.js client bundle at image build time # (Railway Docker ARG), same as NEXT_PUBLIC_POSTHOG_*. Changing it requires a # web rebuild, not only a runtime env update. # Frontend: NEXT_PUBLIC_SENTRY_DSN= NEXT_PUBLIC_SENTRY_ENVIRONMENT=development NEXT_PUBLIC_SENTRY_RELEASE= # Backend services (API, Worker, Runtime): set a distinct DSN per service so # events stay separated. Runtime events are expurgated before send. SENTRY_DSN= SENTRY_ENVIRONMENT=development # Optional immutable deploy identifier shared by backend services for release correlation: SENTRY_RELEASE= # Optional auth token for uploading source maps during the web build only: # SENTRY_AUTH_TOKEN= # --- S3-compatible storage (MinIO locally) --------------------------------- # API owns normal storage writes. Runtime also needs GetObject for selected Skill archives; use a # separate read-only credential in production even though the variable names are the same per service. S3_ENDPOINT=http://127.0.0.1:9000 MINIO_PORT=9000 MINIO_CONSOLE_PORT=9001 S3_REGION=us-east-1 S3_ACCESS_KEY_ID=companion S3_SECRET_ACCESS_KEY=companion-secret S3_BUCKET_SKILL_ARCHIVES=skill-archives S3_FORCE_PATH_STYLE=true # --- Email ------------------------------------------------------------------ # Verification + password-reset codes and org invites are sent through this provider. Use "mailpit" # (read codes in the Mailpit UI) or "log" (console) locally; set "resend" + RESEND_API_KEY in production. EMAIL_PROVIDER=mailpit EMAIL_FROM=Companion MAILPIT_SMTP_HOST=127.0.0.1 MAILPIT_SMTP_PORT=1025 MAILPIT_WEB_PORT=8025 RESEND_API_KEY= # --- Billing (SaaS only; self-hosted defaults stay fully unlocked) ---------- COMPANION_BILLING_MODE=disabled COMPANION_ENTITLEMENTS_MODE=off COMPANION_ENTITLEMENT_PILOT_ORGS= COMPANION_PRO_ORG_ALLOWLIST= COMPANION_CHECKOUT_ENABLED=false COMPANION_STRIPE_WEBHOOKS_ENABLED=false STRIPE_SECRET_KEY= STRIPE_WEBHOOK_SECRET= STRIPE_PRO_PRICE_ID= STRIPE_PORTAL_CONFIGURATION_ID=