Bun monorepo for exploring the next generation of the Supabase CLI and local development stack.
Before you open a pull request:
- Open an issue first, using one of the issue templates.
- Wait for maintainer triage. A maintainer categorizes the issue (
✨ Feature,🐛 Bug, or📘 Docs) and adds theopen-for-contributionlabel once it is ready to be worked on. - Open a pull request only after the
open-for-contributionlabel is set, and link the issue with a closing keyword (for exampleCloses #123).
Until the open-for-contribution label is present, the issue is still in triage, so work should not start and a pull request should not be opened.
Pull requests from external contributors that do not follow this workflow are commented on and closed automatically by the Contribution Gate. Supabase members are exempt, so they can work from Linear tickets that are not public on GitHub. Maintainers: see .github/MAINTAINERS.md.
This repo pins the versions of Node, Bun, Go, pnpm, and golangci-lint that contributors are expected to build against, and uses mise — a polyglot version manager — to install and activate them automatically. If you don't already have these tools installed, mise is a great way to get up and running quickly.
# macOS / Linux
curl https://mise.run | sh
# macOS via Homebrew
brew install miseSee the mise installation docs for other package managers (apt, dnf, cargo, npm, Windows, …).
mise needs to hook into your shell so it can inject the right tool versions into your PATH as you move between directories. Follow the mise activate instructions in this section to add the activation line for your shell to its startup file.
This repo relies on mise support for reading Node and pnpm versions from package.json, so use mise 2026.7.0 or newer.
Trust this repo's mise.toml once from the repo root so mise can read the project setting that enables idiomatic version files:
mise trustThen install the pinned tool versions:
mise installmise install resolves the versions this repo expects from a handful of files, rather than hardcoding them all in one place:
| Tool | Version source |
|---|---|
| Bun | .bun-version |
| Node.js | devEngines.runtime field in package.json |
| pnpm | packageManager field in package.json |
| Go | mise.toml |
| golangci-lint | mise.toml |
The Go and golangci-lint entries in mise.toml are intentionally temporary while the Go CLI remains in the repo. The canonical Go module metadata still lives in apps/cli-go/go.mod; keep the mise.toml entries aligned only until the Go code is removed.
Once installed, mise activates these versions automatically whenever your shell is inside this repo — no manual nvm use, gvm use, or similar switching required.
mise is not required. If you already have Bun, Node, pnpm, and Go installed and managed some other way, just make sure your versions match the ones pinned in .bun-version, mise.toml, package.json, and apps/cli-go/go.mod.
Install workspace dependencies:
pnpm installClone the reference submodules used during development:
bun run repos:installThat pulls .repos/effect/, which is the local source of truth for Effect v4 APIs and patterns in this repo.
.
|-- apps/
| |-- cli/ # Published Supabase CLI package
| `-- docs/ # Next.js docs site generated from the CLI
|-- packages/
| |-- api/ # Typed Supabase Management API client
| |-- config/ # Supabase config schema and generated types
| |-- process-compose/ # Effect-based process orchestration library
| |-- stack/ # Programmatic local Supabase stack runtime
| `-- cli-*/ # Platform-specific CLI binary packages
|-- tools/ # Repository tooling (release scripts, etc.)
|-- docs/ # ADRs, design notes, and implementation docs
`-- .repos/effect/ # Effect v4 reference source
| Workspace | Purpose |
|---|---|
apps/cli |
Main supabase package. Contains command handlers, runtime services, auth, output, telemetry, and docs generation scripts. |
apps/cli-e2e |
Compatibility e2e test suite. Record-and-replay harness for testing the TS Legacy port against real Supabase Management API responses. |
apps/docs |
Internal docs site built with Next.js and generated from the CLI docs sources. |
| Workspace | Purpose |
|---|---|
packages/api |
Auto-generated TypeScript client for the Supabase Management API. |
packages/cli-test-helpers |
CLI test harness library — createHarness/exec API for spawning TS Legacy and TS Next CLI subprocesses in tests. |
packages/config |
JSON Schema and generated TypeScript types for Supabase configuration. |
packages/process-compose |
TypeScript/Bun port of process-compose used for multi-service orchestration. |
packages/stack |
Programmatic local Supabase stack used by the CLI and other tooling. |
packages/cli-darwin-arm64 |
Published native CLI binary wrapper for macOS arm64. |
packages/cli-darwin-x64 |
Published native CLI binary wrapper for macOS x64. |
packages/cli-linux-arm64 |
Published native CLI binary wrapper for Linux arm64 (glibc). |
packages/cli-linux-arm64-musl |
Published native CLI binary wrapper for Linux arm64 (musl). |
packages/cli-linux-x64 |
Published native CLI binary wrapper for Linux x64 (glibc). |
packages/cli-linux-x64-musl |
Published native CLI binary wrapper for Linux x64 (musl). |
packages/cli-windows-x64 |
Published native CLI binary wrapper for Windows x64. |
Root-level scripts:
pnpm run repos:install
pnpm run repos:pull
pnpm run build # build CLI, config, and docs artifacts with Turbo
pnpm run generate # generate API, then documentation artifacts with Turbo
pnpm run check:all # run all checks across every project
pnpm run fix:all # run all fixers across every projectStandard TypeScript workspaces (apps/cli-e2e, apps/cli, packages/api, packages/cli-test-helpers, packages/config, packages/process-compose, packages/stack) declare their package scripts explicitly. Test suites vary by package: unit tests are standard, while integration and e2e tests exist only where applicable.
| Script | What it does |
|---|---|
test |
Run the package's declared test suites |
test:unit |
Run unit tests |
test:integration |
Run integration tests where applicable |
test:e2e |
Run end-to-end tests where applicable |
types:check |
Type-check with tsc --noEmit |
The test and type-check scripts are declared in each package's package.json, so package-local commands are directly discoverable and can be sharded independently.
Linting, formatting, and unused-code analysis are repo-wide rather than per-package: oxlint, oxfmt, and knip read .oxlintrc.json, .oxfmtrc.json, and knip.json at the repo root (knip's config maps each workspace under its workspaces key). The root check:all/fix:all scripts are the sole repo-wide quality entrypoints and use Turbo to run the package type checks and root-owned quality scripts. Package-local work can run pnpm types:check and the package's test scripts. Running the tools directly from the repository root also just works:
pnpm exec oxlint
pnpm exec oxfmt
pnpm exec knip-bunPackage-local type checks and tests can be run from the workspace you are changing:
# From a project directory — scoped to that project only:
pnpm types:check
pnpm run test:unit
# If this package declares an integration suite:
pnpm run test:integration
# From the workspace root — repo-wide quality and all-project test fan-out:
pnpm run check:all
pnpm run fix:all
pnpm run test:unit && pnpm run test:integrationThe root unit and integration scripts use Turbo to fan out the package-local
test:*:run tasks across the standard TypeScript/Vitest workspaces. The Go
workspace remains package-local because its tests run directly through Go:
pnpm --dir apps/cli-go run test:unit. Go tests are covered by the dedicated
Go CI workflow. Unit and integration tasks are uncached for now; e2e tasks are
also uncached and run one package at a time. Forward a Vitest shard to every
e2e package with pnpm run test:e2e --shard=1/3.
apps/cli-e2e implements the replay-and-record compatibility harness for the TypeScript Legacy CLI (ts-legacy, the only shipped CLI shell). Live tests are owned by apps/cli and run from the command they cover. The CLI still shells out to the bundled Go binary for the handful of commands the TS port proxies (db diff, db pull, db branch *, db remote *, gen keys, functions download), so apps/cli-go/ is built alongside the TS CLI for these suites, but there is no Go-vs-TypeScript parity runner.
Replay fixtures are recorded by running ts-legacy against the real Supabase staging API and capturing request/response pairs. Replay runs serve those committed fixtures back to the same CLI, so compatibility tests are fast and deterministic with no network access. The replay/record suite remains entirely under apps/cli-e2e.
The replay/record harness has two modes:
| Mode | When | What it does |
|---|---|---|
| Replay (default) | Every PR / local dev | Loads committed fixtures; serves recorded responses to the CLI subprocess. Fast and deterministic — no network access. |
| Record | RECORD=true |
Proxies CLI traffic to staging and captures request/response pairs as fixture files. |
The live suite lives in apps/cli/src/** as collocated *.live.test.ts files and runs in the CLI package's separate, serial live Vitest project. Global setup requires SUPABASE_LIVE_API_URL and SUPABASE_ACCESS_TOKEN, then provisions one uniquely named project through the typed Management API client, waits for it to become healthy, creates the shared storage fixture, and writes a temporary YAML profile. Every live subprocess receives that profile, so the same contract works with Supabox, a Docker-hosted API platform, or staging by changing only the URL and token. Teardown always removes the temporary profile and deletes the exact owned project unless SUPABASE_LIVE_KEEP_PROJECT=1 is set.
The configured URL is the Management API endpoint. Tenant data-plane URLs keep
the CLI profile contract (https://<ref>.<project_host>) using the host derived
from the provisioned project's database metadata.
Live coverage is smoke coverage, not an exhaustive command matrix. Add one representative golden-path test for each user-facing command, colocated beside that command. A live test should assert one target command; setup and teardown may invoke other commands when they prepare or clean up state, but those commands are not asserted in that test. Keep validation, formatting, fallback, error, and matrix details in integration tests unless the remote/runtime boundary itself is the behavior under test. See ADR 0013 and apps/cli/live.env.example.
To run the live suite locally, copy apps/cli/live.env.example, set the API URL and access token for the target platform, and run the root Turbo entrypoint. Its build dependency prepares the CLI artifacts before Vitest starts:
pnpm run test:liveOptional SUPABASE_LIVE_ORG_ID, SUPABASE_LIVE_REGION, and
SUPABASE_LIVE_PROJECT_NAME values select provisioning details. Set
SUPABASE_LIVE_KEEP_PROJECT=1 only when debugging a failed run; the temporary
profile is still cleaned up.
Live CI is manual or daily scheduled and is not PR-blocking; run it manually on a PR branch when you need pre-merge remote coverage.
# Replay mode — fast, no credentials needed
pnpm exec turbo run @supabase/cli-e2e#test:e2e:run # ts-legacy targetRecording proxies CLI traffic to the Supabase staging API. Provide a staging access token and a project ref for commands that need one — everything else is baked into the script:
SUPABASE_ACCESS_TOKEN=<your-staging-token> SUPABASE_TEST_PROJECT_REF=<your-project-ref> SUPABASE_STAGING_URL=<stagingUrl> pnpm run recordReview the generated files in apps/cli-e2e/fixtures/recorded/ before committing — verify that no real tokens, UUIDs, or project refs appear (they should be replaced with __ACCESS_TOKEN__, __UUID__, __PROJECT_REF__ placeholders).
After recording, replay must pass with no changes against the freshly committed fixtures:
pnpm exec turbo run @supabase/cli-e2e#test:e2e:runA test failing only after a recording session usually means an assertion needs updating to match the CLI's current real-world output, not the fixture.
apps/cli-e2e/fixtures/
├── recorded/ # Committed fixture pairs, captured from real staging responses
│ └── <KEY>/ # e.g. GET_v1_projects/
│ ├── default.request.json
│ └── default.response.json
├── errors/ # Manually crafted error fixtures (401, 403, 404, …)
└── scenarios/ # Reserved for stateful workflow tests (Tier 2)
Fixture files must never contain real tokens, UUIDs, or project IDs. The recording step replaces all dynamic values with stable placeholders automatically.
Test code imports from @supabase/cli-test-helpers (packages/cli-test-helpers):
import { createHarness, exec } from "@supabase/cli-test-helpers";
const harness = createHarness("ts-legacy", { apiUrl, accessToken });
const result = await exec(harness, ["projects", "list"]);Test a real end-to-end publish and install of the CLI against a local npm registry (Verdaccio), without touching npm and without modifying any git-tracked files.
- Bun — for compiling the CLI binary and running the scripts
- Go — only required for
--legacyshell (commands proxied to the Go binary) - pnpm — already required by this repo
- Node.js — required by
npx/npm install -gto test the published package
Terminal 1 — start the local registry:
pnpm local-registryThis starts Verdaccio on http://localhost:4873 and creates a publish user. Your global npm and pnpm registry config is never modified — every command that talks to the local registry passes --registry explicitly. Press Ctrl+C when done.
Terminal 2 — build and publish:
# Publish the next (TypeScript-native) shell
pnpm cli-release --next
# Or publish the legacy (Go-backed) shell
pnpm cli-release --legacy
# Pin a specific version (default: 0.0.0-local.<epoch-seconds>)
pnpm cli-release --next --version 0.0.0-local.1The script builds the CLI binary for the current platform only, compiles the Node.js shim, and publishes two packages to the local registry:
@supabase/cli-<platform>@<version>— the compiled binarysupabase@<version>— the shim that resolves and execs the binary
No git-tracked files are modified. Build output goes to a system temp directory that is deleted after publish.
# Run directly with npx
npx --registry http://localhost:4873 supabase@0.0.0-local.1 --version
# Or install globally and run as `supabase`
npm install -g --registry http://localhost:4873 supabase@0.0.0-local.1
supabase --version| Problem | Fix |
|---|---|
Error: Something is already running on port 4873 |
Kill the leftover Verdaccio process (lsof -ti:4873 | xargs kill) and retry |
go not found in PATH (legacy only) |
Install Go from https://go.dev/dl/ |
Error: Go CLI source not found (legacy only) |
Run pnpm repos:install to clone apps/cli-go |
npm / pnpm tries to fetch from localhost:4873 when no registry is running |
Stale global registry override left behind by an older version of local-registry.ts (the current script never modifies global config). Run npm config delete registry and pnpm config delete registry. Note that pnpm stores the override in its own global config (~/Library/Preferences/pnpm/auth.ini on macOS, ~/.config/pnpm/ on Linux), not ~/.npmrc — check there if the delete command fails |
npx resolves from npm instead of local |
Pass --registry http://localhost:4873 explicitly to npx / npm install |
Turbo owns repository task execution and dependency graph orchestration. Quality
checks are root-owned check:all/fix:all scripts orchestrated with Turbo,
while ordinary unit, integration, and e2e tests remain package-local scripts;
see Standard package scripts.
Inspect a task's dependency graph with Turbo's JSON dry-run output:
pnpm exec turbo run <task> --dry=jsonBuild all migrated workspaces:
pnpm run buildGenerate API and documentation artifacts:
pnpm run generateBuild only the CLI and its Go sidecar:
pnpm exec turbo run supabase#buildThe CLI build names its config and Go build prerequisites explicitly. The
remaining CLI workspace dependencies intentionally have no build script, so
strict Turbo task selection does not synthesize no-op ^build tasks for them.
Run the live suite:
The target's uncached build dependency prepares the CLI artifacts before Vitest starts; use Turbo for cacheable build outputs.
pnpm run test:liveRun pnpm run check:all or pnpm run fix:all from the repository root for
repo-wide quality checks. Package-local checks use pnpm types:check plus the
package's test scripts.
docs/adr/contains architecture decision records.docs/contains design notes for CLI output, telemetry, environment management, distribution, migration, and monorepo tooling.apps/cli/docs/contains source material used to generate command documentation.
The repo keeps source checkouts in .repos/ for local inspection while developing:
.repos/effect/contains the complete Effect v4 source used as the reference implementation for types, APIs, and patterns.