Skip to main content
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

Use the Node version from .nvmrc (for example nvm use) and let Corepack provide the pinned pnpm:
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.
2

Run the tests

3

Start the emulators from source

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. 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 | 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 [email protected] -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 | | 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 with the Ultracite 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:
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

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

Try it by hand

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.
4

Run the full check before pushing

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:
make start runs the same CLI once, without watching. See Configuration and Saved state 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:
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: 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:
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

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).
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 for what is generated and must not be edited by hand.

How Turborepo runs tasks

Task wiring lives in turbo.json: ^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:
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

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.
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-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.
allowBuilds in pnpm-workspace.yaml lists packages whose build scripts are deliberately disabled (esbuild, sharp, and others). This is expected.
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.
CI installs with pnpm install --frozen-lockfile. Commit pnpm-lock.yaml whenever you add, remove, or change a dependency, including after pnpm new:provider.