This is the full developer documentation for Opslane # Introduction > What Opslane is and where to start. Opslane is an open-source error tracker that fixes the bugs reaching your users. Install the SDK in your app. Opslane turns errors and session recordings into a ranked list of real problems and pull requests that fix them. ## Start here [Section titled “Start here”](#start-here) [Run Opslane locally](/quickstart/self-host/)Start Opslane with Docker Compose and send your first error. [Install the SDK](/install/)Capture errors from React, Vue, or vanilla JavaScript. [How Opslane works](/how-it-works/)Follow an error from the browser to a verified fix pull request. [Connect GitHub](/guides/github-app/)Give Opslane access to investigate and fix your code. Opslane’s server, worker, dashboard, and docs are AGPL-3.0-only. Its browser and Python SDKs and shared types are MIT licensed. # Install the SDK > Install the browser SDK in React, Vue, or vanilla JavaScript, identify users, and label environments. The SDK sends your app’s errors and session recordings to Opslane. Setup is one install command, one `init` call, and one `setUser` call. ## Let your agent do it [Section titled “Let your agent do it”](#let-your-agent-do-it) Paste this into your coding agent: ```text Set up https://docs.opslane.com/INSTALL.md ``` Your agent installs the SDK, identifies signed-in users, verifies an error from your app, and configures source-map uploads. You approve the project in your browser and choose whether to connect GitHub, Slack, CI secrets, and MCP. For manual setup, follow the steps below. Before you start, you need an ingest key for your project. The SDK accepts only keys beginning with `opslane_pk_`. See [API keys](/guides/api-keys/). The key ships in your bundle, so keep it in an environment variable rather than in committed code. > **Privacy:** session recording is on by default. Review [replay privacy and masking](/guides/replay-privacy/) before you deploy. Self-hosted servers need PostgreSQL with the `vector` extension. Compose supplies `pgvector/pgvector:pg16`; RDS operators must check their engine’s extension support. See [server prerequisites and upgrades](/quickstart/self-host/#prerequisites). ## Install [Section titled “Install”](#install) ```bash npm install @opslane/sdk ``` React and Vue are optional peer dependencies, so you install whichever one your app already uses. ## Initialize [Section titled “Initialize”](#initialize) Call `init` once, as early as possible in your browser entry point. Then call `setUser` after sign-in. `init` installs handlers for uncaught errors and unhandled promise rejections, instruments `console`, `fetch`, and `XMLHttpRequest`, records click and submit interactions, and starts session recording. Calling it twice is a no-op, and the SDK never throws into your code. ### React [Section titled “React”](#react) ```tsx import { createRoot } from 'react-dom/client'; import { init, setUser } from '@opslane/sdk'; import { OpslaneErrorBoundary } from '@opslane/sdk/react'; import App from './App'; init({ apiKey: 'opslane_pk_...', environment: import.meta.env.VITE_OPSLANE_ENVIRONMENT ?? 'development', // Self-hosting? Add endpoint: 'https://your-opslane-instance.example.com'. // Hosted Opslane needs no endpoint: the SDK defaults to https://app.opslane.com. }); // After sign-in. setUser({ id: currentUser.id, email: currentUser.email }); createRoot(document.getElementById('root')!).render( Something went wrong.

}>
); ``` The error boundary catches render errors, which React does not surface to `window.onerror`. Everything else, including event handlers, `setTimeout`, and promise rejections, goes to the global handlers `init` installs. ### Vue 3 [Section titled “Vue 3”](#vue-3) ```ts import { createApp } from 'vue'; import { init, setUser, opslaneVuePlugin } from '@opslane/sdk'; import App from './App.vue'; init({ apiKey: 'opslane_pk_...', environment: import.meta.env.VITE_OPSLANE_ENVIRONMENT ?? 'development', // Self-hosting? Add endpoint: 'https://your-opslane-instance.example.com'. // Hosted Opslane needs no endpoint: the SDK defaults to https://app.opslane.com. }); setUser({ id: currentUser.id, email: currentUser.email }); createApp(App).use(opslaneVuePlugin).mount('#app'); ``` The plugin hooks `app.config.errorHandler`, keeping any handler you already registered, and tags each error with the failing component’s name and lifecycle hook. ### Next.js [Section titled “Next.js”](#nextjs) Initialize Opslane in a client component after hydration. Route SDK requests through your app so a Content-Security-Policy allowing `connect-src 'self'` covers them and ad blockers are less likely to drop them. Add this rewrite to `next.config.*`, preserving any existing rewrites: ```ts async rewrites() { return [{ source: '/opslane/api/v1/:path*', destination: 'https://app.opslane.com/api/v1/:path*', }]; } ``` For a self-hosted deployment, replace the destination origin with your Opslane address. SDK requests omit browser credentials. If you have middleware, ensure it does not add `Authorization` or `Cookie` headers to `/opslane/*`. Set `NEXT_PUBLIC_OPSLANE_API_KEY` in your gitignored `.env.local`, then create `app/opslane-provider.tsx`: ```tsx 'use client'; import { useEffect } from 'react'; import { init } from '@opslane/sdk'; export function OpslaneProvider({ children }: { children: React.ReactNode }) { useEffect(() => { const apiKey = process.env.NEXT_PUBLIC_OPSLANE_API_KEY; if (!apiKey) throw new Error('NEXT_PUBLIC_OPSLANE_API_KEY is not set: add it to .env.local and restart the dev server'); init({ apiKey, endpoint: '/opslane', environment: process.env.NEXT_PUBLIC_OPSLANE_ENVIRONMENT ?? 'development', }); }, []); return <>{children}; } ``` Then wrap `{children}` with `` in `app/layout.tsx`. Call `setUser` from a client component after sign-in, as you would in React or Vue. If your app already has a client-side authentication provider, you can initialize Opslane there instead of adding a separate provider. ### Vanilla JavaScript [Section titled “Vanilla JavaScript”](#vanilla-javascript) ```ts import { init, setUser } from '@opslane/sdk'; init({ apiKey: 'opslane_pk_...', environment: import.meta.env.VITE_OPSLANE_ENVIRONMENT ?? 'development', // Self-hosting? Add endpoint: 'https://your-opslane-instance.example.com'. // Hosted Opslane needs no endpoint: the SDK defaults to https://app.opslane.com. }); setUser({ id: 'user-123' }); ``` ## Always call setUser [Section titled “Always call setUser”](#always-call-setuser) Every independently built bundle that calls `init()` must also call `setUser()` after authentication: the main app, embeds, iframe apps, and portal or extension panels each need their own call. A bundle that skips `setUser` reports every user as anonymous. Anonymous sessions can still contribute to error impact, but Opslane cannot connect repeat activity to the same person or account, and anonymous activity cannot start a standalone session-recording issue. The dashboard flags this with **No user identification**. When it names one bundle or application, check its entry point. ## Content-Security-Policy [Section titled “Content-Security-Policy”](#content-security-policy) If your app sends directly to hosted Opslane, include its origin in your existing policy: ```text connect-src 'self' https://app.opslane.com; ``` Preserve other origins your app needs. For self-hosting, use your own Opslane origin. The Next.js tunnel above only needs `'self'`. Replay uploads also need the storage origin returned by your deployment; a blocked request appears in the browser console. ## Set the environment [Section titled “Set the environment”](#set-the-environment) `environment` labels where the SDK data came from. Without it, everything lands in the project’s default environment, which starts as `production`, so staging traffic reads as production. See [environments](/guides/environments/). Set the variables at build time. For Vite: ```bash VITE_OPSLANE_API_KEY=opslane_pk_... VITE_OPSLANE_ENVIRONMENT=staging ``` For Next.js, use `NEXT_PUBLIC_OPSLANE_API_KEY` and `NEXT_PUBLIC_OPSLANE_ENVIRONMENT` and read them from `process.env`. Keep the ingest key in your deploy platform or CI secret store rather than the repository. Browsers can read the key from the built bundle, but a committed key is slow to rotate. ## Capture errors yourself [Section titled “Capture errors yourself”](#capture-errors-yourself) ```ts import { captureException, clearUser } from '@opslane/sdk'; try { riskyThing(); } catch (err) { captureException(err instanceof Error ? err : new Error(String(err))); showFallbackUI(); } clearUser(); // on logout ``` Throw real `Error` objects rather than strings. A string throw arrives with no stack frames, and Opslane classifies it as `unfixable_no_app_frames` ([reason codes](/reference/reason-codes/)). ## Upload source maps [Section titled “Upload source maps”](#upload-source-maps) Production stacks point at minified bundles until you upload source maps. For Vite, add the `opslane()` plugin and set `OPSLANE_SOURCEMAP_KEY`; see [source maps](/guides/source-maps/). For Next.js and other bundlers, run `opslane-sourcemaps` after the production build, as described in the same guide. ## Serve cross-origin scripts correctly [Section titled “Serve cross-origin scripts correctly”](#serve-cross-origin-scripts-correctly) If your bundle is served from a different origin than your page, add `crossorigin` to the script tag. Without it, browsers report `Script error.` with no stack, and Opslane drops those events as noise. ```html ``` ## Verify [Section titled “Verify”](#verify) Temporarily render a button that throws `new Error('opslane-test')`, then click it. Use `onClick={() => { throw new Error('opslane-test'); }}` in React or Next.js, `@click="() => { throw new Error('opslane-test') }"` in Vue, or `onclick="throw new Error('opslane-test')"` in HTML. The error should appear as an issue within a few seconds. Delete the test button after Opslane receives it. If nothing arrives, check the key prefix, the `endpoint` value on self-hosted installs, and the browser console for SDK warnings (set `debug: true` to see them). Every `init` option, with types and defaults: [SDK options](/reference/sdk-options/). ## Optional operator usage notifications [Section titled “Optional operator usage notifications”](#optional-operator-usage-notifications) Self-hosted operators can set `USAGE_EVENTS_SLACK_WEBHOOK` on both server-side services to send best-effort notifications to a Slack incoming webhook. When unset, usage notifications are fully disabled. The notifications cover user signup and login, an environment’s first SDK event, issue admission, fix PR creation, needs-human outcomes, delivered digests, and successful MCP tool calls. Delivery is fire-and-forget and is not an audit log. > **Privacy:** These messages can contain customer email addresses and error titles. Send them only to a private channel whose membership matches your production-data access policy, and store the webhook as a deployment secret. # Self-host quickstart Run Opslane locally with Docker Compose. This is **developer self-hosting**: the default Compose file uses development credentials and is not a production deployment (production operations are tracked separately). There are two paths, depending on which credentials you have. Both start the same way. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Docker with Compose * Ports `8082` (API + dashboard), `5434` (Postgres), and `9012` (MinIO) free on your machine Compose uses `pgvector/pgvector:pg16`. An external PostgreSQL server must make the `vector` extension available and permit the migration role to create it; migration 078 runs `CREATE EXTENSION IF NOT EXISTS vector`. On Amazon RDS, check the [extension matrix for your exact engine version](https://docs.aws.amazon.com/AmazonRDS/latest/PostgreSQLReleaseNotes/postgresql-extensions.html) and enable `vector` with an authorized database role before upgrading. Local Compose is the verified deployment path here; these instructions do not claim an RDS deployment test. No other tools are required for Path 1. Nothing here needs Node, Go, or pnpm; everything runs in containers. ## Start the stack [Section titled “Start the stack”](#start-the-stack) ```bash git clone https://github.com/opslane/opslane.git cd opslane docker compose up -d --wait curl http://localhost:8082/health ``` `docker compose up -d --wait` starts Postgres, MinIO (replay storage), the Opslane API service (`ingestion`, which also serves the dashboard at ), and the worker, and returns once they report healthy. A one-shot `migrate` service applies all database migrations automatically; you do not run migrations by hand. If the `curl` returns `{"status":"ok"}`-style output with HTTP 200, the stack is up. > **Port conflict?** If `docker compose up` reports “port is already allocated”, another service holds 8082/5434/9012. `docker ps` will show the holder. Each port has an override, so you can move the whole stack instead of stopping the other service: > > ```bash > INGESTION_PORT=8092 OPSLANE_POSTGRES_HOST_PORT=5444 OPSLANE_MINIO_HOST_PORT=9022 docker compose up -d --wait > curl http://localhost:8092/health > ``` > > Put those in `.env` if you want them to stick: every later `docker compose` command in this directory needs the same values, or the services disagree about where to find each other. The compose command’s output is the source of truth; a healthy response on 8082 can come from a different app entirely, so always check `docker compose ps` shows *these* services healthy. ## Upgrading a running stack [Section titled “Upgrading a running stack”](#upgrading-a-running-stack) For releases after the known-problems cutover, pull the release and rebuild the services. The one-shot `migrate` service applies schema changes before the new API and worker start. The first upgrade across migration 078 requires the explicit order below: schema ordering alone cannot retire work held by old workers. ### Known-problems cutover (migration 078) [Section titled “Known-problems cutover (migration 078)”](#known-problems-cutover-migration-078) Deploy outside the daily summary window. Keep the same Compose project, volumes, and port settings throughout. These commands use Compose’s bundled database; external-database operators must run the SQL against their configured database. 1. Pull the release and build its `ingestion` and worker images. Start PostgreSQL with the pgvector image, apply the additive schema through migration 078, and deploy the new `ingestion` service first. Keep new workers stopped at this stage. ```bash git pull docker compose build ingestion worker docker compose up -d --wait postgres docker compose run --rm migrate docker compose up -d --no-deps --wait ingestion ``` 2. Stop **every old worker**, across all hosts and replicas. For a single Compose stack, run `docker compose stop worker`. Confirm no old worker process remains; a worker still running a job could otherwise rewrite an archived bucket. 3. Retire old buckets in one transaction: ```bash docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U opslane -d opslane < scripts/retire-friction-buckets.sql ``` This archives groups with `kind='friction'` and no ticket in `candidate`, `queued`, `analyzing`, `awaiting_approval`, `insight`, `needs_human`, or `investigated`, preserving their previous status. It fails their pending or claimed investigation and fix jobs and invalidates cached digest copy. Existing `fixing`, `pr_draft`, and `pr_created` groups remain intact. Delivered digest payloads remain immutable. The script is safe to rerun. 4. Start the new workers, then backfill each intended project and environment. Before a backfill, confirm the worker has `OPENAI_API_KEY`: without embeddings every problem a recording shows becomes a new known problem, and the publish gate never asks whether two problems share one fix. Backfill jobs fail on a worker without the key unless scheduled with `--allow-missing-embeddings`, and the command warns when its own environment lacks it. Start with `--since 1d` and check that recordings of one problem gather on one known problem before running the full lookback. Replace the two UUID placeholders with stored IDs: ```bash docker compose up -d --no-deps --wait worker docker compose exec -T worker node dist/bin/backfill-tickets.js \ --project PROJECT_UUID --environment ENVIRONMENT_UUID --since 14d --rate 60 ``` The CLI schedules jobs at 60 per minute and exits. Workers process them through the ordinary matching pipeline. Reruns skip active jobs and completed finding decisions, including findings judged not to be problems. Partial decisions resume, and completed empty narratives stay skipped. A host checkout can run the same command after a build with `pnpm --filter @opslane/worker backfill:tickets` and `DATABASE_URL` set. 5. Let matching and confirmation drain, then check cause investigations before the next digest. To start an environment’s known-problem list over (for example after a backfill ran without embeddings), run the reset, then the backfill again with the same `--since`: ```bash docker compose exec -T worker node dist/bin/reset-friction-tickets.js \ --project PROJECT_UUID --environment ENVIRONMENT_UUID --since 14d --confirm ``` The reset archives every known problem in the environment and its issue, fails their queued work and pending matching inside the lookback, detaches recordings from the archived issues, and deletes the matching decisions for narratives inside the lookback so the backfill decides them again. Recordings, checks and matches stay for audit. It refuses while a fix is in flight or a fix PR is open. Run the rerun with `FRICTION_MATCH_MAX_CONCURRENT=1`: concurrent matching jobs read the ticket list before taking the environment lock, so two jobs can each create the same new problem. Restore the default when the backfill drains. A digest already frozen for the day keeps its archived cards out but does not pick up the new list until the next run. Model spend for the rerun is roughly the same as the first backfill. Use worker health and job logs to distinguish future scheduled jobs, retries, and failed work. The next digest includes only tickets with qualifying confirmed evidence and a completed cause investigation. `FRICTION_MATCH_MAX_CONCURRENT=2` and `FRICTION_CONFIRM_MAX_CONCURRENT=1` are the fleet-wide defaults. Confirmation permits 2000 recording checks per project per UTC day by default. A backfill can consume model tokens and that daily budget; `--rate` controls scheduled arrivals, not model spend or a global throughput limit. **Fix forward:** after new workers have written atomic findings, API rollback below migration 078 is unsupported: replaying migration 068 against those `friction_signals` rows fails. Set `FRICTION_MATCH_MAX_CONCURRENT=0` and `FRICTION_CONFIRM_MAX_CONCURRENT=0` on every worker and recreate the workers to pause matching, confirmation, reconciliation, and new publication while preserving data. These caps do not undo work already committed or stop unrelated job types. Resume by restoring the caps after deploying the correction. The daily summary is the one surface a mid-flight upgrade can visibly change. A run picks its issues, writes the summary, and checks it a few minutes later. That is normally a ten-minute window around 09:00 in the project’s own timezone, and longer on a day a run is retrying. An upgrade landing inside that window can leave a run whose text was written by one version and checked by another. Any item that fails the check falls back to its plain mechanical lines for that day, so the reader sees the same issues described more tersely rather than losing them, and the next morning’s run writes them normally again. Deploy outside a project’s summary window if you would rather not spend that day. ## Path 1: capture and group an error, no credentials [Section titled “Path 1: capture and group an error, no credentials”](#path-1-capture-and-group-an-error-no-credentials) **What this proves:** your app can send an error and Opslane captures and groups it, without any AI or GitHub account. Seed a test project and its ingest key, then send a fake error: ```bash docker compose exec -T postgres psql -U opslane -d opslane < scripts/seed-e2e.sql curl -X POST http://localhost:8082/api/v1/events \ -H 'Content-Type: application/json' \ -H 'X-API-Key: opslane_pk_mzxw6ytboi3damrrgi3tknzxgq_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq' \ -d '{"timestamp":"2026-01-01T00:00:00Z","error":{"type":"ReferenceError","message":"demo is not defined","stack":"ReferenceError: demo is not defined\n at app.js:1:1"},"breadcrumbs":[],"context":{"url":"https://example.com","user_agent":"smoke test"},"sdk_version":"0.0.1","platform":"javascript"}' ``` The `opslane_pk_...` value is the seed script’s test ingest key, quoted at the top of `scripts/seed-e2e.sql`; real deployments create their own. You should get HTTP `202` back. Give it a few seconds to show up as an issue, then check it: ```bash docker compose exec -T postgres psql -U opslane -d opslane \ -c "SELECT status, reason_code, reason_message FROM error_groups ORDER BY created_at DESC LIMIT 1;" ``` Expected result: ```text status | reason_code | reason_message --------+-------------+---------------- new | | ``` The event was captured and grouped into a `new` issue. Opslane doesn’t investigate a one-off that hasn’t reached enough users, and without AI credentials it couldn’t investigate anyway. That takes Path 2. Once an error reaches enough users, Opslane reads your repository and decides whether to investigate it. An investigation ends one of three ways: a pull request with a fix, a note that the cause is outside your code, or a stop with a reason for you to take over. This quickstart uses two kinds of keys. The seeded `opslane_pk_` **ingest key** can only send events and recordings, and is safe to ship in a browser bundle. Uploading source maps takes a separate `opslane_sk_` **source-map key**, created with the key-creation command (`mint-key`); see the [source maps guide](/guides/source-maps/). ## Path 2: full error-to-PR [Section titled “Path 2: full error-to-PR”](#path-2-full-error-to-pr) **What this proves:** the complete loop: error in, investigated, fix written, evidence collected in a sandbox, pull request out, labeled ready or draft to match its evidence. Requires all of the following set in your environment **before** `docker compose up`: | Variable | What it’s for | Where to get it | | --------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `ANTHROPIC_API_KEY` | AI investigation and fix generation | [console.anthropic.com](https://console.anthropic.com) | | `E2B_API_KEY` | Sandbox where fixes are built and verification evidence is collected before delivery | [e2b.dev](https://e2b.dev) | | `GITHUB_TOKEN` | Cloning the repo, opening the PR, and reading its CI result | GitHub Settings > Developer settings > fine-grained PAT with repository contents and pull requests write access, plus checks and commit statuses read access | | `OPSLANE_E2B_JAVASCRIPT_TEMPLATE` | The sandbox image every JavaScript job boots from. Without it, every job fails at startup. | Build it once with `packages/worker/e2b-javascript/build.ts`; see that directory’s README | You also need a **target repository the worker may open PRs against**. Use a fork of a small fixture app (e.g. this repo’s `test-fixtures/vue-app` pushed to a scratch repo), never a production repo you aren’t ready to receive AI PRs on. Point your project’s `github_repo` at it (via the dashboard, or by editing the seeded project row). ```bash # export the four variables above in your shell, then: docker compose up -d --wait ``` Opslane only investigates an issue once it has reached enough real users recently, so drive the error from several distinct sessions with current timestamps, not a single one-off. Send errors that originate from code in that repository (install [`@opslane/sdk`](https://github.com/opslane/opslane/blob/main/packages/sdk/README.md) in the fixture app, or resend a stack trace that matches its files). Watch the job: ```bash docker compose logs -f worker ``` A verified automatic run opens a draft pull request containing the fix and its evidence. Automatic pull requests stay draft even when repository CI passes. A fix that a person starts can open ready for review after it reproduces the problem and verifies the change. Projects may also opt in to draft delivery for a more limited, independently reviewed fix when the available checks show no regression. Runs that cannot safely progress stop with a reason. The worker never opens a ready-for-review pull request without executed verification evidence. The same contract is exercised by `test-e2e/error-to-pr.test.ts`, which skips itself unless `ANTHROPIC_API_KEY` and `GITHUB_TOKEN` are present. ## The 15-minute definition [Section titled “The 15-minute definition”](#the-15-minute-definition) When we say this quickstart takes under fifteen minutes, the timer is defined as: * **Start:** the `git clone` command begins, on a machine with Docker installed and images *not* pre-pulled, on a residential-class connection. * **End (Path 1):** the `SELECT` above shows a `new` row (the event captured and grouped). * Prerequisites (installing Docker, creating accounts for Path 2 credentials) are outside the timer. ## Cleanup [Section titled “Cleanup”](#cleanup) ```bash docker compose down # stop the stack, keep data docker compose down -v # stop and delete all local data (destructive) ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) * **Event returns 401:** the `X-API-Key` value doesn’t match a key in the database. Copy the full `opslane_pk_...` value exactly as it appears above (or in `scripts/seed-e2e.sql`); a truncated or edited key can’t parse. If the seed never ran, run it now; running it twice is safe. * **Event returns 403 `insufficient_scope`:** the key is real but has the wrong scope, usually an `opslane_sk_` source-map key pasted where the ingest key belongs. Send events with the `opslane_pk_` key. * **Job stays pending:** check `docker compose ps`: the worker container must be up and healthy. `docker compose logs worker` shows job-start and completion lines. Container health only proves the process answers, so if it is green and the job still sits there, ask the worker directly with `docker compose exec worker node -e "fetch('http://localhost:8081/health').then(r=>r.text()).then(console.log)"`. A `stalled` status means work is eligible and no worker is starting it; `queue_depth` separates jobs that are eligible now from ones held back by retry backoff. * **`minio-setup exited (1)` and `ingestion` never starts:** the stack stops on purpose instead of hanging. Read the `minio-setup:` lines above the exit; they name the check and the fix. The usual cause is another Compose stack holding port 9012 (check `docker ps --filter publish=9012`), in which case set `OPSLANE_MINIO_HOST_PORT` to a free port and re-run. Otherwise `docker compose logs minio` has the real error. * **Dashboard shows a login page you can’t get past:** dashboard sign-in uses GitHub OAuth and needs a GitHub App configured (`GITHUB_APP_CLIENT_ID`, `GITHUB_APP_CLIENT_SECRET`) plus `DASHBOARD_ORIGIN=http://localhost:8082`, all set before `docker compose up`. Without `DASHBOARD_ORIGIN`, a successful GitHub sign-in redirects to port 3000, where nothing is listening in this setup. Path 1 doesn’t require the dashboard. # Environment variables > Every environment variable each service reads. Every variable each service actually reads, from `os.Getenv` (Opslane server) and `process.env` (worker). The [drift check](https://github.com/opslane/opslane/blob/main/scripts/check-docs-drift.mjs) fails the repository test gate (`pnpm test`, which CI runs) if code and this page disagree. ## Browser SDK build [Section titled “Browser SDK build”](#browser-sdk-build) These variables are read only by the Vite build process; none is shipped as a secret to the browser. | Variable | Required | Purpose | | ----------------------- | -------- | -------------------------------------------------------------------------------- | | `OPSLANE_COMMIT_SHA` | no | First environment override for the commit recorded in source-map build metadata. | | `GITHUB_SHA` | no | GitHub Actions commit fallback. | | `VERCEL_GIT_COMMIT_SHA` | no | Vercel commit fallback. | | `CF_PAGES_COMMIT_SHA` | no | Cloudflare Pages commit fallback. | | `CI_COMMIT_SHA` | no | GitLab and compatible CI commit fallback. | | `RENDER_GIT_COMMIT` | no | Render commit fallback. | | `BITBUCKET_COMMIT` | no | Bitbucket commit fallback. | | `GIT_COMMIT` | no | Generic CI commit fallback. | | `BUILD_SOURCEVERSION` | no | Azure Pipelines commit fallback. | The precedence is the plugin’s explicit `commitSha`, then the variables in the table order, then `.git/HEAD`. Only lowercase 40- or 64-character hexadecimal values are accepted. COMMIT\_REF is intentionally unsupported because many CI systems use it for a branch name. ## Opslane API [Section titled “Opslane API”](#opslane-api) | Variable | Required | Purpose | | -------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DATABASE_URL` | yes | Postgres connection string | | `PORT` | no (8080) | HTTP listen port | | `JWT_SECRET` | yes | Signs session tokens and derives the notification destination encryption key (≥32 bytes). Rotating it invalidates stored webhook configs; users must re-enter their webhook URLs. | | `OPSLANE_PUBLIC_INGEST_URL` | for creating source-map keys | Public Opslane server origin that source-map uploads should reach. Read by `cmd/mint-key`, which stores it in every `-scope sourcemaps` key it creates; a build therefore configures uploads with that one key and no endpoint variable. Must be an absolute origin (https, or http only for loopback) with no path, query, or fragment. `-endpoint` overrides it and must agree if both are set. | | `AUTH_PROVIDER` | no | Identity provider: `github` (default) or `workos`. Selection is explicit and invalid/partial WorkOS configuration fails boot. | | `AUTH_CALLBACK_ORIGIN` | no | Public Opslane server origin used to construct the allowed `/auth/callback` URL. Never derived from the request Host header. Defaults to the local server port; Compose sets `http://localhost:8082`. | | `WORKOS_API_KEY` | when `AUTH_PROVIDER=workos` | WorkOS secret API key used for AuthKit code exchange. | | `WORKOS_CLIENT_ID` | when `AUTH_PROVIDER=workos` | WorkOS project client ID used for AuthKit authorization. | | `AUTH_WORKOS_SOCIAL` | no | Comma-separated social login buttons to show under `AUTH_PROVIDER=workos` (e.g. `google,github`). UI capability only; the WorkOS dashboard governs which methods actually work. | | `DASHBOARD_DIR` | no | Directory of built dashboard SPA to serve (set in the Docker image) | | `DASHBOARD_ORIGIN` | no | Allowed dashboard origin for CORS **and** the OAuth redirect target. For the bundled Compose setup, set `http://localhost:8082`. This is separate from the worker’s `DASHBOARD_URL`, which supplies links in pull requests and notifications. | | `DASHBOARD_URL` | no | Public or private HTTP(S) dashboard base URL used for notification links shown to users. Configure it explicitly; loopback URLs are rejected, and `DASHBOARD_ORIGIN` is not used as a fallback. This mirrors the worker variable of the same name. | | `USAGE_EVENTS_SLACK_WEBHOOK` | no | Slack incoming webhook for best-effort operator usage notifications. Unset disables them. Configure the same private-channel webhook on both server-side services. | | `AUTUMN_SECRET_KEY` | no | Autumn secret key. Billing is completely disabled when this is unset, empty, or whitespace-only. | | `AUTUMN_BASE_URL` | no (`https://api.useautumn.com`) | Autumn API base URL. Override only for testing or an Autumn-compatible deployment. | | `AUTUMN_PRO_PLAN_ID` | no (`pro`) | Autumn plan ID attached by the Pro checkout flow. | | `AUTUMN_FREE_PLAN_ID` | no (`free`) | Autumn plan ID automatically enabled when a customer is first created. | | `BILLING_SWEEP_INTERVAL_SECONDS` | no (300) | How often the server reports durable merged-PR outcomes to Autumn and checks operator-only session ceilings. Invalid values use the default. | | `BILLING_SESSION_ALERT_THRESHOLD` | no (5000) | Monthly session count above which the server emits one operator alert per organization. This alert is plan-unaware and does not enforce a limit. | | `GROUPING_DEBUG_ID_FRAMES` | no (`false`) | Uses valid SDK build identifiers to normalize temporary JavaScript stack-frame locations whose bundle URL changes between page loads. A build identifier is embedded in a built file and its source map so Opslane can match them. Only the literal `true` enables this setting, and the server reads it once at startup. Keep it consistent across server replicas. After source-map processing, Opslane groups each error into its final issue. | | `NOTIFY_UNSAFE_EXTRA_WEBHOOK_HOSTS` | no | **Development/test only.** Comma-separated exact `host[:port]` additions to the Slack webhook allowlist; added hosts may use HTTP. Never set this in production. | | `GITHUB_APP_ID` | for GitHub App | App ID | | `GITHUB_APP_CLIENT_ID` | for OAuth sign-in | OAuth client ID | | `GITHUB_APP_CLIENT_SECRET` | for OAuth sign-in | OAuth client secret | | `GITHUB_APP_PRIVATE_KEY` | for GitHub App | App private key (PEM) | | `GITHUB_APP_SLUG` | no | App slug used in install URLs | | `GITHUB_WEBHOOK_SECRET` | for webhooks | HMAC secret for webhook verification | | `REPLAY_STORE_ENDPOINT` / `REPLAY_STORE_PUBLIC_ENDPOINT` | for replays | S3-compatible endpoint (internal / browser-visible) | | `REPLAY_STORE_ACCESS_KEY` / `REPLAY_STORE_SECRET_KEY` | for replays | Storage credentials | | `REPLAY_STORE_BUCKET` / `REPLAY_STORE_REGION` | for replays | Bucket and region | | `INTERNAL_READ_TOKEN` | for worker replay evidence | Shared secret guarding recording-part reads from the worker to the Opslane server. Unset disables the internal endpoint. | | `SESSION_IDLE_CLOSE_MINUTES` | no (30) | Idle minutes before a recording session closes and the server queues its `session_analysis` job to detect problems in the recording | | `RETENTION_SWEEP_INTERVAL_SECONDS` | no (3600) | How often a background task closes idle sessions and removes expired recordings | | `PRIORITY_SCORE_INTERVAL_SECONDS` | no (1800) | How often the server recomputes priority scores for open incidents and finds pages whose importance has not yet been assessed. Must be a positive integer number of seconds; invalid values use the default. | | `SCRUB_INTERVAL_SECONDS` | no (15) | How often a background task looks for completed parts of recordings that need redaction. Tests may shorten it. | | `RESOLVE_SWEEP_INTERVAL_SECONDS` | no (300) | How often a background task retries source-map processing and, after waiting too long, continues with the minified stack trace. | | `IDENTITY_SETTLE_INTERVAL_SECONDS` | no (5) | How often the server groups reported errors after source-map processing finishes. | | `FILTER_SWEEP_INTERVAL_SECONDS` | no (30) | How often the server checks uninvestigated errors for enough recent users in enabled environments, then queues a short repository review that decides whether to start a full investigation. Invalid values use the default. | | `MCP_FRAME_URL_TTL` | no (`15m`) | Lifetime of signed session-frame URLs returned by MCP tools. Use a positive Go duration such as `5m` or `1h`. Invalid values use the default. | | `ADMIN_EMAILS` | no | Comma-separated operator email allowlist for the cross-tenant admin dashboard. Empty disables the admin API. Docker Compose maps it from the host-side `OPSLANE_ADMIN_EMAILS`. | | `VERSION` | no | Reported by `/health` | The Opslane server reads **only** the `REPLAY_STORE_*` names; `MINIO_*` names appear in its test code, not runtime configuration. ## Worker [Section titled “Worker”](#worker) | Variable | Required | Purpose | | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DATABASE_URL` | yes (hard exit without it) | Postgres connection string | | `INGESTION_BASE_URL` | for session replay evidence | Opslane server base URL used to fetch decoded recording parts with sensitive values removed | | `INTERNAL_READ_TOKEN` | for session replay evidence | Shared secret sent to the Opslane server as `X-Internal-Token` | | `ANTHROPIC_API_KEY` | for investigation | Claude API access; jobs that require it dead-letter as operator configuration failures when it is missing. | | `INVESTIGATION_MODEL` | no (`claude-sonnet-5`) | Anthropic model used by the codebase-aware diagnosis pass. Unknown model names use the default pricing estimate for budget enforcement. | | `DIGEST_MODEL` | no (`INVESTIGATION_MODEL`, then `claude-sonnet-5`) | Anthropic model used to write daily-summary items selected by the server. | | `DIGEST_WRITER_MAX_WRITES` | no (unlimited) | Most daily-summary items one run may write from scratch. Items reused from an earlier day cost nothing and are always delivered; anything over the limit is held for the next day with a stated reason. `0` delivers only reused items. A value that is not a whole number of zero or more runs unlimited and logs one warning. Compose passes this through; on AWS, add it to the worker task definition in the deploy repository or it has no effect there. | | `INQUIRY_MODEL` | no (`INVESTIGATION_MODEL`, then `claude-sonnet-5`) | Anthropic model used for a short, read-only repository review that decides whether an issue that met the review threshold warrants a full investigation. | | `PRODUCT_CONTEXT_MODEL` | no (`INVESTIGATION_MODEL`, then `claude-sonnet-5`) | Anthropic model used to refresh Opslane’s understanding of your pages and user actions after a default-branch push. | | `PRODUCT_CONTEXT_MAX_TURNS` | no (route-scaled, max 80) | Overrides the route classifier’s model/tool-step limit. Without an override it scales from 20 with the number of routes. | | `PRODUCT_CONTEXT_BUDGET_USD` | no (route-scaled, max 3.00) | Overrides the route classifier’s estimated model-spend ceiling. Without an override it scales from $0.50 with the number of routes. | | `FIX_JUDGE_MODEL` | no (`claude-sonnet-5`) | Second Anthropic model that reviews a fix after tests run. Automated fixes stop when this model does not approve them. | | `INVESTIGATION_MAX_TURNS` | no (10) | Maximum model and tool steps allowed for one investigation. | | `INVESTIGATION_BUDGET_USD` | no (2.00) | Estimated model-spend ceiling for one investigation. It is a runaway backstop, not an operating target. Crossing it stops the attempt without inventing a conclusion. | | `FRICTION_INVESTIGATION_MODEL` | no (`claude-sonnet-4-6`) | Anthropic model that reviews whether a session recording shows a real user problem. | | `FRICTION_INVESTIGATION_MAX_TURNS` | no (30) | Maximum model and tool steps for exploring the repository before the model must submit a conclusion. Zero stops without a conclusion. | | `FRICTION_INVESTIGATION_BUDGET_USD` | no (2.00) | Estimated model-spend ceiling in USD for investigating a problem detected from a session recording. Exceeding it stops the investigation without publishing a cause. | | `E2B_API_KEY` | for verification | Sandbox where fixes are tested | | `GITHUB_TOKEN` | one of the two GitHub modes | PAT for clone + PR | | `GITHUB_APP_ID` / `GITHUB_APP_PRIVATE_KEY` | the other mode | GitHub App installation tokens | | `DASHBOARD_URL` | no | Public or private HTTP(S) dashboard base URL used for incident links in PR bodies and notifications. Configure it explicitly; loopback URLs are rejected, and the Opslane server’s `DASHBOARD_ORIGIN` is not used as a fallback. | | `USAGE_EVENTS_SLACK_WEBHOOK` | no | Slack incoming webhook for best-effort operator usage notifications. Unset disables them. Messages can contain customer email addresses and error titles, so use a private channel. | | `AUTUMN_SECRET_KEY` | no | Autumn secret key. Worker quota checks are a complete no-op when this is unset, empty, or whitespace-only. | | `AUTUMN_BASE_URL` | no (`https://api.useautumn.com`) | Autumn API base URL used by worker quota checks. | | `AUTUMN_FREE_PLAN_ID` | no (`free`) | Autumn plan ID automatically enabled when the worker first touches an organization. | | `WORKER_ID` | no (generated) | Stable worker identity used when one worker takes temporary ownership of a job | | `POLL_INTERVAL_MS` | no (5000) | How long the worker waits when the queue is empty (it drains continuously while work exists). Accepted range 50-300000; out-of-range or non-integer values log a warning and fall back to the default | | `SHUTDOWN_GRACE_MS` | no (25000) | Maximum time to wait for the poll loop during shutdown. Must stay below the platform’s container termination grace period, or the container is killed before the graceful path runs. Compose sets `stop_grace_period: 30s` on the worker for this reason; Docker’s 10s default is *below* the 25s grace. Accepted range 1000-120000; out-of-range values log a warning and fall back to the default | | `LEASE_DURATION_MS` / `REAPER_INTERVAL_MS` / `SILENCE_CHECK_INTERVAL_MS` | no | Duration of a worker’s temporary job ownership, expired-job cleanup interval, and stalled-queue check interval | | `OPSLANE_BUILD_SHA` | no (`unknown`) | Git commit the worker image was built from. CI sets it through the image build argument of the same name, and every agent run log records it so a run can be rebuilt with the code that produced it. Agent run logs are off when `LEASE_DURATION_MS` is below 60000 or object storage is not configured | | `RESOLVE_AGE_DAYS` | no (14) | Inactivity period before eligible human-review or completed-analysis issues resolve automatically | | `INACTIVITY_CHECK_INTERVAL_MS` | no (900000) | How often the worker sweeps for inactive issues (15 minutes by default) | | `SESSION_ANALYSIS_MAX_CONCURRENT` | no (2) | Fleet-wide cap on `session_analysis` jobs running at the same time; `0` prevents workers from starting analysis jobs. Raising it only helps if a worker also runs enough concurrent loops to use the extra room; see `WORKER_CONCURRENCY` | | `WORKER_CONCURRENCY` | no (process 1, Compose 4) | How many jobs one worker process runs at once (any job type, mixed). Accepted range 1-16; invalid or out-of-range values log a warning and fall back to 1, or clamp to 16. The simultaneous-analysis ceiling is `min(SESSION_ANALYSIS_MAX_CONCURRENT, replicas × WORKER_CONCURRENCY)`. Narration, frame checks, matching and confirmation have their own caps; investigation and fix jobs do not, so they scale with this value times the replica count. | | `OPENAI_API_KEY` | no | OpenAI key for `text-embedding-3-small` ticket retrieval. Without it, new tickets have no embedding: the careful first look sees no similar tickets and creates duplicates, and the publish gate never asks the one-fix question. The worker warns at startup, and `backfill-tickets` jobs fail on a worker without it unless scheduled with `--allow-missing-embeddings`. | | `FRICTION_MATCH_MODEL` | no (`claude-haiku-4-5-20251001`) | Model for matching recorded behavior to known problems; uses the narrative API key and endpoint, with Anthropic fallbacks. | | `FRICTION_FIRST_LOOK_MODEL` | no (`claude-sonnet-5`) | Strong review model for newly reported problems. | | `FRICTION_FIRST_LOOK_MAX_TOKENS` | no (16384) | Output token ceiling for the careful first look. The request timeout grows with it (20 ms per token, at least 120 seconds). Invalid values, or values outside 1024 to 32000, fall back to the default. | | `FRICTION_MATCH_MAX_CONCURRENT` | no (2) | Fleet-wide cap on running `friction_match` jobs. `0` pauses new matches. | | `FRICTION_CONFIRM_MODEL` | no (`claude-sonnet-5`) | Model for checking known problems against recordings and screenshots and deciding whether one fix covers two problems. Recording checks keep the model’s default thinking with a 16,000-token limit. | | `FRICTION_MAX_OPEN_FIX_PRS` | no (5) | Maximum open fix PRs per project for automatic fixes. Manual requests remain available. | | `FRICTION_FOLD_MIN_SIMILARITY` | no (0.75) | Embedding similarity floor for duplicate candidates at the publish gate; each candidate still needs a yes to the one-fix question before a fold. | | `FRICTION_INSIGHT_INVESTIGATE_USERS` | no (5) | Identified users whose confirmed recordings an insight (kind `ux_insight`) needs before it is investigated automatically. Defects are investigated on publication. Invalid values fall back to 5. | | `FRICTION_CONFIRM_DAILY_CAP` | no (2000) | Maximum recording checks per project per UTC day across all workers. A read’s single retry uses no extra unit; job retries count again, and finished checks resume without using more budget. `0` pauses checks until the limit is raised. | | `FRICTION_CONFIRM_MAX_CONCURRENT` | no (1) | Fleet-wide cap on running `friction_confirm` jobs. `0` pauses confirmation and publication reconciliation. | | `NARRATIVE_API_KEY` | when session narratives are enabled | Model API key for session narratives and frame verification. Falls back to `ANTHROPIC_API_KEY`. Without either key, narrative reservations remain pending. | | `NARRATIVE_MODEL` | no (`claude-sonnet-5`) | Model used to write session narratives and verify findings against captured frames. Both use the model’s default thinking (adaptive on Claude Sonnet 5); frame verification allows at least 16,000 output tokens so thinking cannot cut its answer off. | | `NARRATIVE_BASE_URL` | no (Anthropic default) | Alternate Anthropic-compatible endpoint for narrative and frame-verification calls. | | `NARRATIVE_MAX_TOKENS` | no (8192) | Maximum output tokens for a narrative call. Values below 1024 use the default. | | `NARRATIVE_APP_CONTEXT` | no | Application details included in the session-narrative prompt. | | `NARRATIVE_DAILY_CAP` | no (2000) | Per-project daily cap on distinct sessions that may reserve narrative or frame-verification calls. | | `NARRATIVE_RENDER_BUDGET_MS` | no (60000) | Wall-clock limit for rendering one replay timeline before the model call. | | `NARRATIVE_MAX_CONCURRENT` | no (2) | Fleet-wide cap on concurrent `session_narrate` jobs. `0` pauses this job type. | | `FRAMES_MAX_CONCURRENT` | no (1) | Fleet-wide cap on concurrent `session_verify_frames` jobs. `0` pauses this job type. | | `NARRATIVE_MONTHLY_BUDGET_USD` | no | Optional per-project estimated monthly spend ceiling for narrative calls. It takes effect only when both narrative token-price variables are valid numbers. | | `NARRATIVE_COST_PER_MTOK_INPUT` | with narrative monthly budget | Estimated input-token price in USD per million tokens. | | `NARRATIVE_COST_PER_MTOK_OUTPUT` | with narrative monthly budget | Estimated output-token price in USD per million tokens. | | `HEALTH_PORT` | no (8081) | Health endpoint port. The worker’s `/health` returns `status: ok`, `stalled` (eligible work waiting, no jobs started in the last minute, and none running), or `unknown` (no queue sample has landed, or the last one is stale). It also returns `claims_per_minute`, a per-job-type `queue_depth`, and `dead_letters_24h` grouped by job type and failure class. `dead_letters_given_up` counts the dead letters the worker will never re-run again (three requeues spent); it never ages out, so a stuck incident stays visible until an operator acts. Queue and dead-letter counts are sampled once a minute, not every time a worker starts a job. | | `REPLAY_STORE_ENDPOINT` / `REPLAY_STORE_ACCESS_KEY` / `REPLAY_STORE_SECRET_KEY` / `REPLAY_STORE_BUCKET` | for replay analysis | Reading stored replays | | `MINIO_ENDPOINT` / `MINIO_ACCESS_KEY` / `MINIO_SECRET_KEY` / `MINIO_BUCKET` | legacy aliases | Worker-side fallback names for the same settings | | `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` / `LANGFUSE_BASE_URL` / `LANGFUSE_PROJECT_ID` | no | Optional LLM tracing | | `ANTHROPIC_BASE_URL` | no (Anthropic default) | Alternate Claude API endpoint; automated tests use it for a fake model server | | `OPSLANE_SANDBOX_BACKEND` | no (`e2b`) | Fix-verification sandbox backend; `local` is only for trusted automated reliability tests and also requires `OPSLANE_RELIABILITY_HARNESS=1` | | `SANDBOX_LIFETIME_MS` | no (`1800000`) | Wall-clock ceiling for a verification sandbox. Values below `900000` fall back to the default; values above `1800000` are clamped to it (E2B enforces account-tier maximums). The ceiling is not billed unless consumed; raising it increases orphan exposure if the worker crashes. | | `OPSLANE_E2B_JAVASCRIPT_TEMPLATE` | when `E2B_API_KEY` is set | Immutable E2B template name built by `packages/worker/e2b-javascript/build.ts`. On the default `e2b` backend, the worker refuses to start when `E2B_API_KEY` is set and this is missing. | | `FIX_SANDBOX_EGRESS_DISABLED` | no | Emergency escape hatch. Set to `1` to leave JavaScript fix sandboxes on unrestricted egress; Python fix sandboxes remain unrestricted until their dependency hosts are measured. | | `OPSLANE_PYTHON_PIPELINE` | no (off) | Routes Python errors through the Python-specific fix workflow for `1` or `true`; Opslane saves the chosen platform with the repair task. | | `OPSLANE_E2B_PYTHON_TEMPLATE` | no (`opslane-python`) | Overrides the E2B template name used by Python repair tasks. | | `OPSLANE_RELIABILITY_HARNESS` | no | Explicit guard required before tests can use the local process runner, which does not isolate commands | | `OPSLANE_GITHUB_URL` | no (`https://github.com`) | Alternate git host for clones; used by tests and self-hosted git | | `OPSLANE_GITHUB_API_URL` | no (GitHub default) | Alternate GitHub REST API base URL for PR creation | The worker starts with only `DATABASE_URL` and logs a warning for missing `ANTHROPIC_API_KEY`, `E2B_API_KEY`, and `GITHUB_TOKEN`. A half-configured E2B worker with `E2B_API_KEY` but no JavaScript template exits at startup. Work that needs another missing credential dead-letters as an operator configuration failure. ## Set in Compose but consumed by no code (known dead config) [Section titled “Set in Compose but consumed by no code (known dead config)”](#set-in-compose-but-consumed-by-no-code-known-dead-config) | Variable | Status | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `VITE_OPSLANE_RELEASE` | Read by no code in this repository. It fed the published legacy uploader’s `release`; that plugin now throws, and debug IDs replaced release matching. Your own application may still read it to pass `init({ release })`, which is display metadata only. | | `ALLOW_REGISTRATION` | Read by nothing; there is no self-serve registration path (sign-in is GitHub OAuth). | | `OPSLANE_ADMIN_EMAILS` | Host-side name that docker-compose.yml maps into the Opslane server service (`ingestion`) as `ADMIN_EMAILS`; consumed by Compose interpolation, not read by code directly. | | `INGESTION_PORT` | Host port published for the Opslane API and dashboard (default 8082). Compose interpolation only. `AUTH_CALLBACK_ORIGIN` follows it unless set explicitly. | | `OPSLANE_POSTGRES_HOST_PORT` | Host port published for the bundled Postgres (default 5434). Compose interpolation only. Set it, plus a matching `DATABASE_URL`, to run a second stack beside an existing one. | | `OPSLANE_MINIO_HOST_PORT` | Host port published for the bundled MinIO (default 9012). Compose interpolation only. `REPLAY_STORE_PUBLIC_ENDPOINT` follows it unless set explicitly. Browsers upload replay chunks to that origin, so the two must agree. | | `OPSLANE_INFRA_BIND_ADDR` | Interface the Postgres and MinIO host ports bind to (default `127.0.0.1`; they carry committed dev credentials). Compose interpolation only. Widen it only when loopback is inside a VM, as with Colima or a remote `DOCKER_HOST`. | | `OPSLANE_MINIO_READY_TIMEOUT_SECONDS` | How long `minio-setup` waits for MinIO before exiting non-zero with a diagnostic (default 60). Compose interpolation only. | | `ENCRYPTION_KEY` | Reserved for future encrypted-at-rest token storage; not read by current code. | # HTTP routes > Every registered HTTP route with its authentication mode. All routes registered by the Opslane API (`packages/ingestion/handler/routes.go`). Auth column legend: **none** (public), **poll token** (`X-Opslane-Poll-Token` for one agent setup session), **SDK** (`X-API-Key` project-scoped public ingest key; rate-limited per project, and origin-gated on browser requests), and **session** (signed-in dashboard session). These are curated tables, not a stability contract. The API is early-stage and may change. The [drift check](https://github.com/opslane/opslane/blob/main/scripts/check-docs-drift.mjs) fails the repository test gate (`pnpm test`, which CI runs) if this page and `routes.go` disagree. ## Public [Section titled “Public”](#public) | Method | Path | Auth | Purpose | | -------- | -------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | GET | `/health` | none | Liveness + dependency checks | | GET | `/metrics` | none | Internal metrics | | POST | `/auth/refresh` | none | Rotate session tokens | | GET | `/auth/config` | none | Discover embedded sign-in, sign-up, and password-reset capabilities | | POST | `/auth/password` | none | Sign in with provider-managed email and password; issues local session cookies | | POST | `/auth/signup` | none | Create a provider account and begin required email verification | | POST | `/auth/verify-email` | none | Complete email verification and issue local session cookies | | POST | `/auth/oauth/verify-email` | flow cookie + same origin | Complete a hosted OAuth email challenge and resume sign-in | | POST | `/auth/password/forgot` | none | Send a password-reset email with an enumeration-safe response | | POST | `/auth/password/reset` | none | Set a new password from a reset token and revoke local refresh sessions | | GET | `/auth/login` | none | Begin the configured identity-provider sign-in | | GET | `/auth/github` | none | Compatibility redirect to `/auth/login` | | GET | `/auth/callback` | none | Configured identity-provider callback | | GET | `/auth/github/callback` | none | Compatibility callback alias for existing GitHub App configurations | | GET+POST | `/oauth/authorize` | none | Begin an authorization request using PKCE | | POST | `/oauth/token` | none | Exchange a PKCE authorization code for a session token | | POST | `/api/v1/agent/setup` | none | Register a two-hour setup session with `project_name`, optional `agent_name` and `git_remote`; return the approval link and poll token | | GET | `/api/v1/agent/poll/{sessionID}` | poll token | Read approval status, approved keys, and server facts; `wait=0..30` long-polls approval, or the first event with `until=event` | | GET | `/agent/auth/{sessionID}` | none | Redirect to the dashboard approval page; expired links return 410 | | POST | `/api/v1/github/webhook` | HMAC | Receive GitHub `pull_request` and default-branch `push` events; both require `X-GitHub-Delivery` (400 without it). State-based `installation` and `installation_repositories` events keep installation records current without a delivery ID. | | POST | `/mcp` | MCP key in `Authorization: Bearer ...` | Call the remote MCP tools for one project | Agent setup uses normal dashboard sign-in and an explicit approval. Approval creates or attaches a project and seals ingest, MCP, and source-map keys to the session. GitHub is an optional later integration. The agent uses only `X-Opslane-Poll-Token` for polling and session actions; expired sessions return 410 before facts or keys. Agent responses use `Cache-Control: no-store`. ## Agent approval and session actions [Section titled “Agent approval and session actions”](#agent-approval-and-session-actions) | Method | Path | Auth | Purpose | | ------ | ---------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | GET | `/api/v1/agent/approve/{sessionID}` | session | Read proposed setup, available projects, suggested match, and bound project progress | | POST | `/api/v1/agent/approve/{sessionID}` | session; admin on cloud | Approve a new project or `existing_project_id` owned by the active organization | | POST | `/api/v1/agent/approve/{sessionID}/deny` | session; admin on cloud | Decline a pending setup | | POST | `/api/v1/agent/github/{sessionID}/install-url` | session; admin on cloud | Create a GitHub App installation URL and callback state for the session’s organization | | GET | `/api/v1/agent/poll/{sessionID}/state` | poll token | Read server facts and reported progress; `wait=0..30`, `until=event`, `github`, `slack`, or `change` (default) | | POST | `/api/v1/agent/poll/{sessionID}/github` | poll token | Validate and attach `{repo}`. Errors include 400 `repo_not_in_installation` with `add_repo_url`, 400 `github_not_installed` with `github_connect_url`, 409 `github_installation_gone` after retirement, and 503 `github_unreachable` with `Retry-After`. | | POST | `/api/v1/agent/poll/{sessionID}/slack` | poll token | Store an encrypted webhook disabled, test delivery, then enable; failed tests remove the disabled destination | | POST | `/api/v1/agent/poll/{sessionID}/progress` | poll token | Report SDK, MCP, and pull-request progress, or failed/skipped diagnostics for server-derived steps | | POST | `/api/v1/agent/poll/{sessionID}/complete` | poll token | Complete onboarding after this session’s first event, or return 422 with `missing: ["first_event"]`; an already-onboarded org still needs the event | ## SDK (X-API-Key) [Section titled “SDK (X-API-Key)”](#sdk-x-api-key) | Method | Path | Origin-gated | Purpose | | ------ | ------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | POST | `/api/v1/events` | browser callers only | Store an error event and queue source-map processing. Receiving an error does not immediately create an issue, investigation, or alert. | | POST | `/api/v1/replays/init` | yes | Begin a replay upload | | POST | `/api/v1/replays/{replayID}/complete` | yes | Finish a replay upload | | POST | `/api/v1/replays/{replayID}/fail` | yes | Record a replay upload failure | | POST | `/api/v1/sessions/init` | yes | Register a tenant-owned session with optional payload `environment`; returns whether the project allows session recording | | POST | `/api/v1/sessions/{sessionID}/chunks/{seq}` | yes | Store and commit one gzipped replay chunk (max 5MiB) | | POST | `/api/v1/ingest/ping` | no | Verify that a public ingest key still authenticates; returns 204 without project data | ## Source-map upload (X-API-Key) [Section titled “Source-map upload (X-API-Key)”](#source-map-upload-x-api-key) | Method | Path | Auth | Purpose | | ------ | ------------------------------ | --------------------- | --------------------------------------------------------------------------------------------------------------------- | | PUT | `/api/v1/sourcemaps/{debugID}` | secret source-map key | Upload one immutable source map after the server verifies its build identifier, which matches the map to a built file | ## Signed-in dashboard [Section titled “Signed-in dashboard”](#signed-in-dashboard) | Method | Path | Purpose | | ------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | GET | `/api/v1/auth/me` | Current user | | GET | `/api/v1/auth/verify` | Validate session | | POST | `/api/v1/auth/logout` | End session | | POST | `/auth/switch-org` | Cloud only: rotate the current session into another member organization | | GET | `/api/v1/invitations` | Cloud org admin: list active-org invitations | | POST | `/api/v1/invitations` | Cloud org admin: create an active-org invitation | | DELETE | `/api/v1/invitations/{invitationID}` | Cloud org admin: revoke an outstanding invitation | | POST | `/api/v1/invitations/accept` | Cloud: accept a single-use, verified-email-bound invitation | | GET | `/api/v1/billing/summary` | Billing-enabled deployments: read the active org’s plan and feature balances (admin on cloud) | | POST | `/api/v1/billing/checkout` | Billing-enabled deployments: start Pro checkout for the active org (admin on cloud) | | POST | `/api/v1/billing/portal` | Billing-enabled deployments: open the active org’s billing portal (admin on cloud) | | GET | `/api/v1/admin/overview` | Operator-only cross-tenant monitoring overview, including best-effort progress through automated repository setup (404 unless allowlisted) | | GET | `/api/v1/admin/jobs` | Operator-only recent jobs (404 unless allowlisted) | | GET | `/api/v1/onboarding/state` | Read server-derived onboarding facts: completion, the newest project, whether any project has an event, and GitHub and Slack connection | | POST | `/api/v1/onboarding/complete` | Mark onboarding complete after any project in the org receives its first event; GitHub and Slack are optional (admin or owner on cloud) | | GET | `/api/v1/projects` | List projects | | POST | `/api/v1/projects` | Create project | | PATCH | `/api/v1/projects/{projectID}` | Update project settings. `friction_autonomy` (`ask_first` or `auto_fix`) controls automatic fixes for session-recording issues; the retired `auto_fix_ux` is accepted as a deprecated alias and stored as `auto_fix`; `pr_posture` controls whether unverified fixes may open as drafts. A same-project `default_environment_id` must be an explicit UUID string when present. | | GET | `/api/v1/projects/{projectID}/fix-stats` | Fix-attempt and pull-request outcome counts by issue type | | GET | `/api/v1/projects/{projectID}/environments` | List all environments, or only environments that contain issues with `used_by=incidents` or sessions with `used_by=sessions` | | GET | `/api/v1/projects/{projectID}/event-count` | Return `has_events` and the nullable `latest_error_group_id` | | GET | `/api/v1/projects/{projectID}/digest/latest` | Latest delivered daily summary, or an empty summary when none has been delivered | | POST | `/api/v1/projects/{projectID}/api-keys` | Create an `api` (MCP), `ingest`, or `sourcemaps` key; secret returned once; only `api` accepts expiry (admin) | | GET | `/api/v1/projects/{projectID}/api-keys` | List the project’s MCP, ingest, and sourcemaps keys without showing their secrets (admin) | | DELETE | `/api/v1/projects/{projectID}/api-keys/{keyID}` | Revoke an MCP, ingest, or sourcemaps key (admin) | | GET | `/api/v1/projects/{projectID}/incidents` | List issues | | GET | `/api/v1/projects/{projectID}/incidents/{incidentID}` | Issue detail | | GET | `/api/v1/projects/{projectID}/incidents/{incidentID}/evidence` | Saved stack frames, failed requests, links to recordings, and available supporting data for the current issue | | GET | `/api/v1/projects/{projectID}/notification-destinations` | List project notification destinations and recent delivery state | | POST | `/api/v1/projects/{projectID}/notification-destinations` | Create a Slack notification destination | | PATCH | `/api/v1/projects/{projectID}/notification-destinations/{destID}` | Update a notification destination | | DELETE | `/api/v1/projects/{projectID}/notification-destinations/{destID}` | Delete a notification destination | | POST | `/api/v1/projects/{projectID}/notification-destinations/{destID}/test` | Send a test notification | | GET | `/api/v1/projects/{projectID}/replays/{replayID}` | Fetch a replay | | GET | `/api/v1/projects/{projectID}/sessions` | List sessions with filters and keyset pagination | | GET | `/api/v1/projects/{projectID}/sessions/{sessionID}` | Session detail and recording metadata with sensitive values removed | | GET | `/api/v1/projects/{projectID}/sessions/{sessionID}/narrative` | Fetch the session narrative, finding grades, and verification timestamp | | GET | `/api/v1/projects/{projectID}/sessions/{sessionID}/chunks/{seq}` | Fetch one decoded, redacted part of the recording | | GET | `/api/v1/projects/{projectID}/incidents/{incidentID}/affected-users` | Affected users | | GET | `/api/v1/projects/{projectID}/incidents/{incidentID}/sample-event` | Fetch the redacted representative event with available source-mapped frames, the raw stack, breadcrumbs, and request context | | POST | `/api/v1/projects/{projectID}/incidents/{incidentID}/fix` | Start a fix for an issue that is ready to fix, whether it came from an error or a session recording. Optional body fields: `guidance` (up to 2000 characters) and `intent`, the signed fix action from a digest link. An intent is scoped to this project and issue; an expired or mismatched intent returns 409 `fix link has expired or no longer matches this issue` | | POST | `/api/v1/projects/{projectID}/incidents/{incidentID}/review` | Request another short repository review for the current issue; reuses an investigation already in progress | | POST | `/api/v1/projects/{projectID}/incidents/{incidentID}/link-pr` | Record a same-repository GitHub pull request without marking the issue resolved | | POST | `/api/v1/projects/{projectID}/incidents/{incidentID}/resolve` | Resolve issue | | POST | `/api/v1/projects/{projectID}/incidents/{incidentID}/archive` | Archive issue | | POST | `/api/v1/projects/{projectID}/incidents/{incidentID}/snooze` | Snooze an actionable issue for up to 30 days; a null or past `until` clears the snooze | | POST | `/api/v1/projects/{projectID}/incidents/{incidentID}/unarchive` | Restore archived issue | | GET | `/api/v1/projects/{projectID}/accounts` | List B2B accounts | | GET | `/api/v1/projects/{projectID}/accounts/{accountID}` | Account detail | | GET | `/api/v1/projects/{projectID}/accounts/{accountID}/incidents` | Issues for one account | | GET | `/api/v1/github/setup` | GitHub App install callback | | GET | `/api/v1/github/status` | GitHub App status: `installed`, `installation_id`, and `install_available`; never creates install state | | POST | `/api/v1/github/install-url` | Start a GitHub App installation for the active organization: returns `install_url` and sets its single-use, 30-minute callback state; admin on cloud; 400 `github_app_not_configured` without an App | | GET | `/api/v1/github/repos` | List installable repos; returns typed 400/409 installation errors or 503 `github_unreachable` with `Retry-After` | | PUT | `/api/v1/projects/{projectID}/github` | Set project repo config; returns 400 `repo_not_in_installation` with `add_repo_url`, 400 `github_not_installed` with `github_connect_url`, 409 `github_installation_gone`, or 503 `github_unreachable` with `Retry-After` | | GET | `/api/v1/projects/{projectID}/github` | Get project repo config | | DELETE | `/api/v1/projects/{projectID}/github` | Remove project repo config | ## Internal service reads [Section titled “Internal service reads”](#internal-service-reads) | Method | Path | Auth | Purpose | | ------ | --------------------------------------------------------------------- | ------------------ | ----------------------------------------------------------- | | GET | `/internal/v1/projects/{projectID}/sessions/{sessionID}/chunks/{seq}` | `X-Internal-Token` | Worker fetch of one decoded, redacted part of the recording | | GET | `/internal/v1/projects/{projectID}/incidents/{incidentID}/status` | `X-Internal-Token` | Read one issue’s status for the deployment smoke test | ## Method mismatch [Section titled “Method mismatch”](#method-mismatch) A request whose path is registered but whose method is not returns a JSON `404`, not a `405`. The router sets one `MethodNotAllowed` handler, so this holds for every route above; an unregistered method is indistinguishable from an unregistered path. Handlers that write `405` themselves (password-reset flows) are unaffected. ## Catch-all [Section titled “Catch-all”](#catch-all) Any other path serves the dashboard SPA from `DASHBOARD_DIR` (missing static assets 404 rather than falling back to `index.html`). # Reason codes > Every handoff reason code with its remediation. When Opslane stops and hands an issue to a person, it records a `reason_code`, a `reason_message`, and a `remediation`. The catalog lives in `packages/worker/src/reason-codes.ts` as an exhaustive `Record`. Adding a code to `shared/src/types.ts` without a remediation entry is a compile error, so this table cannot silently fall behind the type. ## Credentials and access [Section titled “Credentials and access”](#credentials-and-access) | Code | Remediation | | ---------------------- | ------------------------------------------------------------------------------------------------------ | | `missing_github_token` | Connect the GitHub App to this repository (Settings > GitHub) so Opslane can read and open PRs. | | `repo_access_denied` | Confirm the GitHub App is installed on this repository and has read + pull-request permissions. | | `token_decrypt_failed` | Reconnect the GitHub integration; the stored credential could not be decrypted. | | `auth_invalid` | Re-authenticate the GitHub connection; the existing token was rejected as invalid. | | `policy_blocked` | A repository or org policy blocked the change. Review branch-protection / app permissions, then retry. | | `missing_llm_key` | Set the `ANTHROPIC_API_KEY` environment variable on the worker with a valid Anthropic API key. | ## Repository state [Section titled “Repository state”](#repository-state) | Code | Remediation | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `empty_repository` | Push at least one commit to this repository, then retry; there is no branch for Opslane to work from yet. | | `invalid_default_branch` | This repository’s default branch points at a branch that no longer exists. Set a valid default branch in GitHub (Settings > Branches), then retry. | | `unresolvable_head` | Opslane could not determine this repository’s default branch. Check the repository is not in an unusual state, then retry. | ## Investigation and fix quality [Section titled “Investigation and fix quality”](#investigation-and-fix-quality) | Code | Remediation | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `malformed_diff` | Review the error manually; the agent could not produce a valid, applicable diff. | | `verification_failed` | Review the candidate fix manually; Opslane could not verify it satisfied the failing behavior. | | `tests_failed` | Review the candidate diff manually; the test suite still fails, so the fix may be partial or cause regressions. | | `low_confidence_fix` | Review the candidate diff and evidence manually; the candidate did not pass independent fix review or the checks required before Opslane opens a pull request. This code does not gate error diagnosis by confidence. | | `repro_not_achievable` | Review the candidate diff and evidence manually; Opslane could not construct a reliable reproduction test, so it checked the fix against the existing suite but did not call it verified. | | `draft_cap_reached` | Review or close an existing Opslane draft PR, then retry this fix. The candidate diff remains available on the incident. | | `billing_limit_reached` | This organization has used its included fix PRs for the month. Upgrade in Settings > Billing to resume automated fix PRs. | | `budget_exhausted` | Review the error manually; the agent could not complete within its step or budget limits. Consider guiding it with more context. | | `insufficient_context` | Add a replay, breadcrumbs, or a reproduction so Opslane has enough context to investigate. | ## Inputs and artifacts [Section titled “Inputs and artifacts”](#inputs-and-artifacts) | Code | Remediation | | ----------------------- | ------------------------------------------------------------------------------------------------------------------ | | `sourcemap_unresolved` | Upload source maps for this release (see the SDK build plugin) so the stack trace shows original source locations. | | `artifact_fetch_failed` | Retry later; Opslane could not fetch a stored artifact needed to analyze this error. | ## Issues not fixed automatically [Section titled “Issues not fixed automatically”](#issues-not-fixed-automatically) | Code | Remediation | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `triage_unfixable` | Review the error manually; Opslane determined it cannot be fixed with application code changes. | | `unfixable_no_app_frames` | Add the `crossorigin` attribute to your `