> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qlane.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Single repo

> Connect a single-repository app so qlane clones the pull request, builds it, serves it in an isolated sandbox, and tests it with full shell access. The four fields, what you can type in them, and why the start command must serve the production build.

The default mode for web apps that live in one repository and run on their own. For every pull request, qlane clones the pull request's code, installs and builds it, starts it in a fresh sandbox, and connects the agent.

<Info>
  **Name in the product:** this mode is a **Simple sandbox**. The environment page's header calls it that, and when you create the environment it is the **Single service** option. This page says Simple sandbox from here on.
</Info>

## When to use this mode

* Your app is **one repository** that runs on its own: a static site, a front end, or an API that only talks to hosted services.
* You can install, build and serve it with your package manager's scripts.
* You want the agent to test the **exact code in the pull request**, not a separate preview deployment.

If your app needs a database, cache, queue or object store running beside it, use [Docker Compose](/connect/docker-compose) (a **Composed sandbox**) instead. If you already build per-PR previews on Vercel, Netlify or Cloudflare, [Existing previews](/connect/existing-previews) is usually a better fit: the same coverage with no extra build time.

## Set it up

From your project, click **Add environment** and choose **Test every pull request**.

<Steps>
  <Step title="Repo">
    Pick the **Repository** and **Branch** (the default to test against, typically `main`; on a pull request qlane uses its head commit automatically). If the app is not at the repository root, set the **Root directory**. See [When the app is not at the repository root](#root-directory).
  </Step>

  <Step title="Runtime">
    Under **How does this app run?**, choose **Single service**.
  </Step>

  <Step title="Login">
    If your app needs a login, add a test account the agent can sign in with. Skip this step if it has none.
  </Step>

  <Step title="Build">
    Fill in the [four fields](#the-four-fields). qlane reads your `package.json` and suggests values; check them against the rules below, especially [serve the production build](#serve-the-production-build).
  </Step>

  <Step title="Review">
    Check the summary and create the environment.
  </Step>
</Steps>

Then add any variables your app needs to boot (an auth secret, an API base URL) under **Runtime Variables** on the environment's page. Mark sensitive values as **secret** to encrypt them and mask them in the UI. Without the values it needs, the app won't start and the run fails before the agent connects.

<h2 id="the-four-fields">
  The four fields
</h2>

| Field | What it does |
| - | - |
| **Install command** | Runs first. Defaults to `npm install` when you leave it empty. |
| **Build command** | Runs next, and **only if you fill it in**. Left empty, the step is skipped entirely. |
| **Start command** | Runs last, in the background. **Required**: nothing is guessed for you if you leave it blank. |
| **Port** | The port your started server listens on. The preview URL points at it. |

**Install and build must run to completion and exit `0`.** A non-zero exit fails the environment and reports that step's own output, so a failure names the step it came from.

**The start command must keep running.** It is launched in the background as a long-running server. If it exits with an error, the step fails at once and shows its output. If it exits cleanly, nothing is left listening, and the step fails when the 30-second window ends.

<Note>
  **There is no ready-check setting. Your app has 30 seconds to answer.** Once the start command is launched, a request to the preview URL must reach your app within a fixed 30-second window. Build time does not count against it, because the build is a separate step that finishes first. The window measures how long your server takes to begin serving, not how long your project takes to compile.
</Note>

Any response your app returns counts, including a 4xx or a 5xx: an app that is up and failing is a finding for the agent to report, not a reason to keep waiting. A reply from the sandbox's own edge saying nothing is listening yet does not count.

`PORT` is set in the start command's environment to the value of the **Port** field. Some frameworks read it and others ignore it, so treat it as a convenience rather than a guarantee. If your framework takes a port flag or reads a port from its config file, make sure that value and the **Port** field agree.

<h2 id="serve-the-production-build">
  Serve the production build, not the dev server
</h2>

This is the most consequential choice on the screen, and the default answer is the production build:

* **Build command**: your production build.
* **Start command**: the command that serves what the build produced.

A dev server skips the production transforms: minification, bundling, tree-shaking, dead-code and unused-CSS elimination, asset hashing, and build-time inlining of environment variables. A whole class of regressions lives there. A class name works in dev and is stripped from the build. A dynamic import breaks only once bundled. A variable is read at runtime in dev but baked in at build time in production. Point the agent at a dev server and it can't see any of them, so it passes a pull request that breaks the moment it ships.

Dev servers also add machinery production doesn't have: hot-reload sockets, and error overlays that can mask, restyle or swallow a real failure.

Fall back to a dev server only when the repository has no way to serve built output.

<h2 id="bind-to-all-interfaces">
  Listen on `0.0.0.0`, not `127.0.0.1`
</h2>

Most dev and preview servers listen on `localhost` only. That default is invisible on a laptop and fatal in a sandbox: a server bound to loopback inside the sandbox can't be reached from outside it. There is no error message. The 30-second wait expires while the process looks perfectly healthy.

Pass whatever your framework uses to listen on every interface: commonly a `--host 0.0.0.0` flag, sometimes a `HOST=0.0.0.0` variable. A [Composed sandbox](/connect/docker-compose#the-app-listens-on-127-0-0-1) has the same requirement.

<h2 id="allowed-commands">
  What you can type in the command fields
</h2>

Each command **is** run through `sh -c`. What limits you is the check that runs before it. Because the shell is real, that check is what stands between a command and everything a shell would otherwise let it do. Two restrictions apply before anything runs:

* **The first word must be one of:** `npm`, `npx`, `yarn`, `pnpm`, `bun`, `node`, `next`, `turbo`, `tsc`.
* **These characters are rejected anywhere in the field**, as are line breaks and carriage returns:

  ``; | & ` $ ( ) { } \ < >``

So **you can't chain, redirect or substitute in a field.** `npm run build && npm run postbuild` is refused, so is `cd app && npm start`, and so is anything using `$(...)`.

The remedy is better than the thing it replaces: **put the chain in a `package.json` script and call the script.** The restriction applies to what you type in the field, never to what the script does, so an `&&` inside a package script runs normally.

```json package.json theme={null}
"scripts": {
  "build": "vite build && node scripts/postbuild.mjs",
  "serve": "vite preview --host 0.0.0.0"
}
```

The fields are then simply `npm run build` and `npm run serve`, and both pass.

<h2 id="root-directory">
  When the app is not at the repository root
</h2>

Set the environment's **Root directory** rather than trying to `cd` into the subdirectory, which the [character rules](#allowed-commands) forbid anyway. Commands then run inside that directory, which also lets the package manager find the workspace's own manifest and use the package manager it declares.

## A worked example

A static site built by a bundler, whose `package.json` has a `build` script and a `preview` server:

| Field | Value |
| - | - |
| Install command | `npm ci` |
| Build command | `npm run build` |
| Start command | `npm run preview -- --host 0.0.0.0` |
| Port | `4321` |

Three things in it matter:

* **`npm ci` rather than `npm install`.** It installs exactly what the lockfile pins, which is reproducible and usually faster. It fails outright if the lockfile has drifted from `package.json`; if the first run errors there, switch to `npm install`.
* **The bare `--`.** With npm, it passes `--host` through to the underlying command instead of npm consuming it.
* **The port is the preview server's, not the dev server's.** They are often different numbers in the same project, and **Port** must name the one the start command binds.

To find your own values, read the `scripts` block in `package.json`. The script that serves built output is usually called `preview`, `serve` or `start`, but check what it does rather than trusting the name: a `start` script that runs the dev server is common, and it is the [wrong choice](#serve-the-production-build).

## Variables

* **Your app's own public address isn't provided.** `QLANE_SANDBOX_PUBLIC_URL` is set only in a [Composed sandbox](/connect/docker-compose#public-address-of-your-app). A variable on a Simple sandbox that references `${QLANE_SANDBOX_PUBLIC_URL}` stops the run rather than expanding to nothing.
* **Names starting with `QLANE_` are reserved.** Saving a variable of your own with that prefix is refused.

## Turn on pull-request testing

A new environment starts with only **Mention triggers** on: comment `@qlane-ai test` on a pull request to start a run. To test every pull request automatically, open **Project settings → PR testing**, find the environment under **Per environment**, and turn on **Automatic runs**. See [Triggers](/triggers) for the details, and [Docker Compose](/connect/docker-compose#step-5-turn-on-pull-request-testing) for **Watch paths**, which work the same way here.

## What qlane does on each pull request

1. Provisions a fresh, isolated sandbox.
2. Clones your repository at the pull request's head commit.
3. Runs your install command, then your build command if you set one, then starts your app.
4. Waits up to 30 seconds for the app to answer on the configured port.
5. Connects the agent and runs your active test cases plus any drafts generated from the pull request's diff.
6. Posts a check and, if anything broke, a structured review.

## What the agent can do in this mode

In addition to a real browser, the agent gets:

* **Shell access** inside the sandbox (`bash`, `readFile`). It can inspect logs, check files it wrote, and re-run commands.
* **Code awareness.** It reads the repository to understand failures and write better reproductions.
* **Deep testing.** When stuck, the agent can add temporary instrumentation, re-run, and observe. The sandbox is torn down after the session, so changes don't escape.

## Common gotchas

* **Native dependencies.** Anything that needs a system library should be in your package's install hooks. The sandbox is Linux-based, so macOS-only build steps fail.
* **A slow server start.** The 30-second window covers start-up only, not the build. If your server needs longer to begin serving, move the work into the build step, or serve the prebuilt output with a lighter server.
* **Required variables.** Set the auth secret, API URLs and anything else the app needs under **Runtime Variables**. Without them the app won't boot, and the run fails before the agent connects.

<h2 id="checklist">
  Checklist
</h2>

* [ ] The start command serves the production build, not a dev server ([why](#serve-the-production-build))
* [ ] The server listens on `0.0.0.0`, not `127.0.0.1` ([why](#bind-to-all-interfaces))
* [ ] **Port** is the port the start command actually binds ([fields](#the-four-fields))
* [ ] Every field starts with an allowed command and contains no rejected characters ([rules](#allowed-commands))
* [ ] Anything that needs chaining lives in a package script, not in a field ([rules](#allowed-commands))
* [ ] The app is at the repository root, or **Root directory** is set ([root directory](#root-directory))
* [ ] The server answers within 30 seconds of the start command launching ([fields](#the-four-fields))
* [ ] The app needs no backing services. If it does, use a [Composed sandbox](/connect/docker-compose)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.