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

# Local development

> Set up the toolchain, build the workspace, run emulators from source, and fix the usual snags.

The root `Makefile` is the front door for day to day work. Every target wraps a plain `pnpm` or `node` command, shown next to it below, so you can always run the underlying command yourself.

## Prerequisites

| Tool | Version | Where it is pinned |
| - | - | - |
| Node.js | 24 or later | `.nvmrc` (`24`) and `engines.node` in the root `package.json` |
| pnpm | 11 (currently `pnpm@11.2.2`) | `packageManager` and `engines.pnpm` (`>=11 <12`) in the root `package.json` |

Use the Node version from `.nvmrc` (for example `nvm use`) and let Corepack provide the pinned pnpm:

```bash theme={null}
corepack enable
```

> [!TIP]
>
> `make doctor` checks your Node and pnpm versions, whether dependencies are installed and packages built, and whether ports 4000 to 4012 and 3333 are free. It prints a fix for anything that fails.

## First-time setup

**1. Clone and install**

`make setup` checks the toolchain, installs dependencies, and builds every package.

```bash theme={null}
make setup
# equivalent to
corepack enable
pnpm install
pnpm build
pnpm doctor
```

**2. Run the tests**

```bash theme={null}
make test    # pnpm test; later runs replay unchanged suites from cache
```

**3. Start the emulators from source**

```bash theme={null}
make dev                  # default services, rebuilt and restarted on every change
make dev SERVICE=stripe   # just one (or a comma separated list)
```

## Command reference

Run `make` with no target for this list in your terminal. Every target is a single command, so you can always run the underlying one yourself; CI calls pnpm directly.

| Make target | Underlying command | What it does |
| - | - | - |
| `make setup` | `pnpm install && pnpm build && pnpm doctor` | First run: install, build, check |
| `make doctor` | `pnpm doctor` | Check toolchain, install, build output, and ports |
| `make dev` | `pnpm dev:cli` | Run from source; rebuild and restart on change (`SERVICE=`, `PORT=`) |
| `make start` | `./baseline start` | Run this checkout's CLI once, rebuilding first if sources changed (`SERVICE=`, `PORT=`) |
| `make list` | `./baseline list` | List every emulator with its endpoints |
| `make playground` | `node scripts/playground.mjs` | Scratch project in `.playground/` for trying your build as a user would (`SERVICE=`) |
| `make build` | `pnpm exec turbo run build` | Build every package in dependency order, cached |
| `make build-provider P=stripe` | `turbo run build --filter=@deploybaseline/stripe...` | One provider and what it depends on |
| `make test` | `pnpm test` | Every test suite; unchanged suites replay from cache |
| `make test-changed` | `pnpm test -- --affected` | Only what changed against `main`, plus everything that depends on it |
| `make test-provider P=stripe` | `pnpm test -- --filter=@deploybaseline/stripe` | One provider's full suite, with its gates (`P=stripe,github` for several) |
| `make test-platform` | `pnpm test -- --filter=@deploybaseline/core ...` | Core, testing, cli, graphql, ws, and the state packages |
| `make test-watch P=stripe` | `pnpm --filter @deploybaseline/stripe exec vitest` | Re-run one provider's tests on change |

Add `TEST_FLAGS=--force` to any test target to rerun suites even when the cache has them.
\| `make typecheck` | `pnpm type-check` | `tsc --noEmit` in every package |
\| `make lint` | `pnpm lint` | Biome (Ultracite preset) lint and format check over the repository |
\| `make format` | `pnpm format` | Biome formatting and safe lint fixes |
\| `make check` | see [What CI enforces](/contributing/testing-and-conformance.md#what-ci-enforces) | Everything the CI build job runs, in order |
\| `make ci` | `make check docs-check` | `check` plus the docs job |
\| `make docs` | `npx -p node@24 -p mint@4.2.1059 -c 'mint dev --port 3333'` in `docs/` | Docs site on port 3333 (`DOCS_PORT=`) |
\| `make docs-check` | the same, with `mint validate && mint broken-links` | The docs CI job |
\| `make api-reference` | `pnpm api-reference` | Regenerate `docs/api-reference/` from registered routes |
\| `make api-reference-check` | `pnpm api-reference --check` | Fail on a stale reference or a docs route that does not exist |
\| `make conformance` | `pnpm conformance` | Regenerate `CONFORMANCE.md` and its docs snippets |
\| `make new-provider NAME=acme` | `pnpm new:provider acme` | Scaffold a provider |
\| `make vendor-spec P=stripe` | build, then `node scripts/vendor-spec.mjs stripe` | Refresh a vendored OpenAPI document |
\| `make coverage-matrix P=stripe` | `node scripts/generate-coverage-matrix.mjs stripe` | Rewrite a coverage matrix from the routes |
\| `make spec-allow P=stripe` | `node scripts/generate-spec-allow.mjs stripe` | Write current spec mismatches to `spec-allow.ts` |
\| `make release PKG=stripe BUMP=patch` | `node scripts/release.mjs stripe --bump patch` | Prepare a release branch; see [Releasing](/contributing/releasing.md) |
\| `make clean` | `pnpm clean && rm -rf .turbo` | Remove build output and caches |
\| `make reset` | `make clean`, then remove every `node_modules` | Start over (then `make setup`) |

One letter shortcuts: `make i` (install), `make d` (dev), `make b` (build), `make l` (lint), `make f` (format), `make t` (typecheck), `make c` (check).

Linting and formatting use [Biome](https://biomejs.dev) with the [Ultracite](https://www.ultracite.ai) preset, configured in `biome.jsonc`. `pnpm install` sets up a Husky pre-commit hook that runs `ultracite fix` on staged files through lint-staged, so commits arrive formatted. Rules the repository turns off are listed in `biome.jsonc` with the reason for each.

## The daily loop

**1. Rebuild what you changed**

Packages import each other through their built `dist/` output, so a change in `core` is invisible to providers until `core` is rebuilt. `make build` is cheap after the first run because Turborepo caches unchanged packages.

To rebuild one provider and everything it depends on:

```bash theme={null}
pnpm --filter "@deploybaseline/stripe..." build
```

The test targets build dependencies for you, and `make dev` serves your changes straight from source, so this step is mostly for type checking and scripts that read `dist/`.

**2. Test the provider**

```bash theme={null}
make test-provider P=stripe
# or, for everything your branch touches
make test-changed
```

To iterate on one file, run Vitest directly. Filtered runs skip the coverage and spec gates, so they never block you mid-change:

```bash theme={null}
pnpm --filter @deploybaseline/stripe exec vitest run src/__tests__/sdk.e2e.test.ts
```

**3. Try it by hand**

```bash theme={null}
make dev SERVICE=stripe            # restarts on every save
./baseline start --service stripe  # or one run of this checkout's CLI
```

The inspector for most providers is at `/_inspector` on the service port. To try it the way a user would, with its own config outside the repository's packages, see [Trying your build](#trying-your-build).

**4. Run the full check before pushing**

```bash theme={null}
make check
```

## Running a provider from source

`make dev` (or `pnpm dev:cli`) runs one script, `scripts/dev.mjs`, with its output prefixed `build` and `serve`:

* `build` watches every package's `src/` with the platform's native recursive watcher. Each provider runs from its own `dist/` (the CLI finds the workspace package), so a provider change rebuilds that provider, without declarations, in under a second. A CLI change rebuilds the CLI. The CLI and every provider inline `core`, so a `core` change rebuilds all of them, a few at a time. The `state-*` packages are loaded from their own `dist/`, so a change there rebuilds that package. Edits under `__tests__` are ignored.
* `serve` is `packages/cli/dist/index.js start`. After every good build the script stops it (the CLI stops its providers) and starts it again on the new build.

Save a file in any provider or in `core`, and the emulators restart with the change about a second later. Ctrl+C stops everything. Seeded data is recreated on each restart; pass `--state` to keep it. Provider `dist/` folders are not rebuilt while you work; `make build` refreshes them.

Every `start` flag passes through:

```bash theme={null}
make dev SERVICE=stripe,resend PORT=5000
pnpm dev:cli --service stripe --state .baseline/state.json
```

`make start` runs the same CLI once, without watching. See [Configuration](/configuration.md) and [Saved state](/state.md) for the full option set.

## Trying your build

`./baseline` (`baseline.cmd` on Windows) runs this checkout's CLI. It is a passthrough: every argument reaches the CLI untouched and the working directory is not changed, so anything in `./baseline --help` works and config is found from wherever you run it. That includes your own projects:

```bash theme={null}
./baseline start --service stripe
./baseline validate
cd ~/my-app && ~/code/baseline/baseline start    # your project, this checkout's code
```

Before starting the CLI it compares every package's sources (not `__tests__`) with the time of its last good build, and when something changed it runs `turbo run build` for the changed packages and everything that depends on them, so a `core` change rebuilds the CLI and every provider. Turbo's cache makes an unneeded build cheap. When the build fails it stops rather than start the old build. Two wrapper flags are consumed and never forwarded:

| Flag | Environment | Effect |
| - | - | - |
| `--skip-build` | `BASELINE_SKIP_BUILD=1` | Run `dist/` as it is |
| `--force-build` | `BASELINE_FORCE_BUILD=1` | Build even when the sources look unchanged |

Everything after a `--` belongs to the CLI. `make start` and `make list` go through the wrapper. For a loop that rebuilds and restarts on every save, use `make dev` instead; it skips declarations, so it is faster per change.

`make playground` (or `make playground SERVICE=stripe`) creates `.playground/`, which is gitignored, and runs `./baseline init` in it. Then:

```bash theme={null}
cd .playground
../baseline start
```

That is the same first run a user gets, with your code: its own `baseline.config.json` and `baseline.lock`, and the console with `/docs` on the base port minus one.

## Running the docs locally

```bash theme={null}
make docs
# equivalent to
cd docs && npx -y -p node@24 -p mint@4.2.1059 -c 'mint dev --port 3333'
```

Then open `http://localhost:3333`. Before pushing docs changes, run `make docs-check`, which is what the `Docs (Mintlify)` CI job runs (`mint validate` and `mint broken-links`).

> [!NOTE]
>
> Mintlify refuses odd (non LTS) Node releases, so the make targets fetch Node 24 through npx for it whatever your own Node is. Keep `MINT_VERSION` in the Makefile in step with `.github/workflows/ci.yml`. See [Docs and API reference](/contributing/docs-and-api-reference.md) for what is generated and must not be edited by hand.

## How Turborepo runs tasks

Task wiring lives in `turbo.json`:

| Task | `dependsOn` | Cached | Notes |
| - | - | - | - |
| `build` | `^build` | yes, outputs `dist/**` | Builds every dependency first |
| `dev` | `^build` | no | One package's `tsup --watch`; `make dev` does not use it |
| `type-check` | `^build` | yes | Needs dependencies' `.d.ts` files |
| `test` | `^build` | yes | Reruns only when the package, a dependency's build, or the root `tsconfig.json` changed |
| `clean` | none | no | |

`^build` means "build every workspace dependency of this package first". That is why `pnpm test` at the root always tests against fresh builds, while `pnpm --filter <pkg> test` (which bypasses Turborepo) uses whatever is already in `dist/`. The CLI's `turbo.json` adds its own `build` to `test` (its suites run the built binary) and the root files its tests read: `tests/contracts/`, `baseline.config.example.json`, and the config examples in `README.md`, `docs/`, and `skills/` (each must pass `validate`). Concurrency is `100%` in `turbo.json`, one task per core: more than that made builds, type checks, and tests slower while using twice the memory.

## How tests share the machine

Every Vitest process starts about one worker per core, so running each package's suite at once used to start hundreds of Node processes and could exhaust memory. `pnpm test` runs `scripts/test.mjs`, which splits the cores instead: at most half of them run suites side by side, and each suite gets an equal share of workers (4 suites with 2 workers each on 8 cores). A single suite keeps Vitest's default. It counts only suites that will actually run, so cached suites cost nothing. Override with `BASELINE_TEST_CONCURRENCY` (suites at once), `VITEST_MAX_WORKERS` (workers per suite), or turbo's `--concurrency`. Anything after a second `--` goes to Vitest:

```bash theme={null}
pnpm test -- --affected -- --reporter=verbose
```

Provider suites built with `providerTestConfig` reuse workers across test files (`isolate: false`). A per-file setup from `@deploybaseline/testing` restores spies, stubbed globals and env, and fake timers after each file, and gives each file a fresh spec gate sample budget, so results match a fresh process per file. If a suite's files depend on fresh module state, pass `isolate: true` to `providerTestConfig`.

## Troubleshooting

> **A test passes alone but fails in the full run**
>
> Provider suites share a worker between test files, so a file can see module state another file left behind. Spies, stubbed globals and env, and fake timers are restored for you; anything else (a module level cache, a mutated `process.env` key) is not. Reset it in the test, or pass `isolate: true` to `providerTestConfig` for that package.

> **A change in core or testing has no effect**
>
> The consuming package is reading a stale `dist/`. Rebuild with `make build`, or `pnpm --filter "@deploybaseline/<provider>..." build` for one provider and its dependencies. If it still looks stale, `make clean` and build again.

> **pnpm refuses to install a dependency version**
>
> `pnpm-workspace.yaml` sets `minimumReleaseAge: 2880`, so pnpm will not install a version published less than two days (2880 minutes) ago. Wait, pick the previous version, or, when the fresh version is genuinely needed, add the exact `name@version` to `minimumReleaseAgeExclude`.

> **A package's install script did not run**
>
> `allowBuilds` in `pnpm-workspace.yaml` lists packages whose build scripts are deliberately disabled (`esbuild`, `sharp`, and others). This is expected.

> **Port already in use**
>
> The base port is 4000, or `BASELINE_PORT` or `PORT` when set. With no config, each default service listens on the base port plus its fixed registry slot (GitHub 4001 through Twilio 4012). Stop the other process, or move everything with `--port 5000`. Tests use `startService` from `@deploybaseline/testing`, which listens on a free port, so they do not collide with a running emulator.

> **Lockfile errors in CI**
>
> CI installs with `pnpm install --frozen-lockfile`. Commit `pnpm-lock.yaml` whenever you add, remove, or change a dependency, including after `pnpm new:provider`.


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