Bun monorepo with workspaces under apps/ and packages/. pnpm is the package manager; use
pnpm <script>, never bun run or npm run.
apps/cli— publishedsupabaseCLIapps/docs— Next.js docs sitepackages/api— typed Management API clientpackages/config— published config schema and generated typespackages/stack— local Supabase stack runtimepackages/cli-*— published platform binary wrappers
Use an existing TypeScript/Bun workspace, especially packages/api, as the package-structure
reference. Published apps/cli and packages/config are not private; apps/docs and
packages/cli-* have their own shapes. Generic lint, format, and unused-code tooling is
root-owned. Effect lint covers packages/stack, all files under
apps/cli/src/commands/experimental/stack and apps/cli/src/commands/experimental/compute, the
shared apps/cli/src/shared/compute runtime helpers (excluding embedded starter templates), the
Compute test fixture helper, and apps/cli/src/command-internal/experimental-feature.ts; use the
root scripts for it.
The config vocabulary is settled: CliConfig is the local config document, ProjectConfig is
the hosted-project subset, and CliSettings is CLI runtime settings. Use Cli* for local
checkout concepts and bare Project* for hosted concepts; helpers follow the family they serve.
Use a family-neutral name when a symbol deliberately spans both families. See the
naming ADR and
config loading guide for details, and
config instructions for its separate release train.
Effect V4 source is in .repos/effect/; use it instead of node_modules, with core APIs in
.repos/effect/packages/effect/, test helpers in .repos/effect/packages/vitest/, and migration
notes in .repos/effect/MIGRATION.md. Run pnpm repos:install if it is absent.
- Write new TypeScript runtime code in Effect. Internal helpers return Effects; Promise facades
belong only at public edges. Wrap a foreign Promise once at its leaf with
Effect.tryPromise, pass cancellation when supported, and map failures into typed domain errors. - Effects are reusable: allocate mutable state per execution (
Effect.suspend,Effect.gen, or scoped acquisition), and keepEffect.synctotal by usingEffect.tryfor throwing thunks. - Keep service requirements visible through the type until composition. Provide services with
layers or
Effect.provide; do not hide missing services with casts, nested runtimes, globals, or synchronous adapters. - Use
Scope/acquireReleasefor resources. LimituninterruptibleMaskto the acquisition-to-registration handoff and keep blocking acquisition interruptible withrestore. PreferEffect.forkChildor scope-owned fibers; detached work needs a documented lifetime and completion path. Use nativeDeferred,Latch,Semaphore,Queue,PubSub,Schedule, and race or concurrent combinators instead of waiter arrays, polling sleeps, or shared cancellation flags. - Shared initialization and teardown are single-flight operations: callers join one cached
Effect, fiber, or
Deferred<Exit<...>>; interrupting one waiter must not cancel shared teardown. Effect.callbackowns its full foreign lifecycle: register listeners before starting, resume at most once, and on cancellation remove owned listeners and close or destroy the exact resource.- Expected failures use typed
Data.TaggedErrorandEffect.fail; never throw them inside Effect programs. Defects are impossible invariants. Recover with the narrowestcatchoperator; usecatchCauseonly when recovery intentionally handles defects or interruption, preserve every other cause, and do not use operationalorDie/Layer.orDie. - Preserve
Data.TaggedErrorstring identities inapps/cli/srcandpackages/config/srcwhen renaming classes; class names may change, but tags must remain stable. See the CLI telemetry identity rule. - Use public helpers (
Exit.isSuccess,Option.isSome,Cause.isTimeoutError, and similar) and exhaustiveMatch/predicate helpers for domain variants. Raw._tagis for schema/type definitions, serialization, or genuinely dynamic boundaries only. - Compose schemas with
decodeUnknownEffect,decodeEffect, andencodeEffect, mappingSchemaErrorinto domain errors. Sync codecs are acceptable only at an explicitly synchronous, service-free edge that intentionally throws.
- Fix the underlying design when Effect lint reports a violation. Refactor to native
Effect constructs; do not silence findings with
oxlint-disable, casts, file exclusions, or weaker lint configuration. - A suppression is acceptable only for a demonstrated false positive or an unavoidable foreign-library boundary. Before retaining one, inspect the corresponding Effect API and identify the specific missing capability or behavior that prevents replacement; existing Promise-based code, native API usage, or refactoring effort alone do not justify an exception. Limit it to the specific rule and smallest scope, and explain why a compliant implementation is not possible.
- Passing lint by bypassing its rules does not complete an Effect migration.
Package scripts are the source of truth for leaf workspaces; root-owned Turbo coordinates build,
generation, quality, live, and auxiliary workflows. Inspect dependencies with
pnpm exec turbo run <task> --dry=json.
Keep the local feedback loop fast; full CI runs for ready PRs targeting develop.
- Documentation-only edits need formatting and reference checks. Code changes need relevant type/lint checks and affected unit/integration tests, using package scripts and root-owned tools.
- Before pushing, run formatting and applicable lint checks on all changed files and fix any findings in those files.
- Broaden checks when shared behavior or runtime wiring changes warrant it, or when requested. Run targeted E2E when the subprocess boundary matters; run full E2E only on explicit request.
- Repeat checks only after changes or failures that could affect their result.
- Fix failures caused by the change and report any unresolved validation blockers. Investigate unexpected failures without automatically expanding the task into unrelated repairs.
Use pnpm test only when its scope fits the change; the CLI's aggregate script includes full E2E,
so use pnpm run test:unit and pnpm run test:integration with affected test files locally.
When repo-wide validation is warranted, use root pnpm check:all or pnpm fix:all. These are the
repo-wide quality entrypoints: check:all runs generic and Effect lint, format, knip, and type
checks; fix:all applies generic and Effect lint, format, and knip fixes. Do not use production
as casts to silence type errors.
Use root Turbo entrypoints for live and auxiliary workflows:
pnpm run build
pnpm run generate
pnpm exec turbo run supabase#build
pnpm run test:liveComments exist for the next reader, not as the author's audit trail. Code states what happens; a comment states only the why that the code cannot carry. Most code needs no comment at all.
Write a comment only for an invariant or constraint the types cannot express, an external quirk,
a decision that would otherwise read as a bug, or a pointer to an ADR, SIDE_EFFECTS.md, docs
page, or upstream issue. If the rationale needs more than three lines, move it to documentation.
- Prefer one sentence of JSDoc on exported symbols; use tags such as
@deprecatedand@seewhen they carry useful meaning. - Skip JSDoc on self-explanatory internal helpers and keep inline comments to one or two lines.
- Describe behavior in its own terms and in the present tense.
- Published package exports need a one-line JSDoc summary. This public repository must not include internal context in comments.
- Code narration, provenance/history, evidence trails, ticket IDs as provenance, or meta-commentary on code shape. A ticket ID belongs only in a
TODO(CLI-1234):or when no ADR exists and the ticket is the only home for a decision. - Restatements of docs, section banners, or Go-parity framing. Link to maintained docs instead.
- ALL-CAPS emphasis or words such as “deliberately”, “crucially”, and “exactly”.
Tests should carry intent in their names; comments only explain non-obvious fixture setup. Keep
tool directives and tool-facing JSDoc tags, and give every lint disable a short reason after --.
A source file whose comment lines exceed a quarter of its code lines should move prose into docs.
Use conventional-commit titles: <type>(<scope>): <subject>. Valid scopes are listed in
commitlint.config.js; keep its list synchronized with the mirrored
scope list in the PR lint workflow. Non-release changes use chore, docs, test,
or ci rather than release-triggering types. Do not put validation,
test plans, or check lists in PR descriptions. Public PRs, issues, and code comments must omit
internal metrics (percentages, ratios, or relative changes are fine), vendor/legal/pricing/strategy
details, and competitor names; protocol identifiers such as user-agent strings are fine. Keep
internal context in Linear.
Internal unreleased APIs may be simplified or reshaped; move responsibility to the correct owner and delete obsolete helpers, shims, and parallel paths instead of preserving compatibility scaffolding. Protect shipped interfaces and valuable persistent data; update consumers, tests, and docs when interfaces, ownership, or lifecycle changes.
- Write focused tests that read as stories: arrange, act, assert.
- Assert behavior that matters to consumers, not implementation details. Prefer real parsers and observable outcomes over source-text or registry checks.
- Make assertions meaningful: establish prerequisites, check specific failures, and choose matchers that express the intended contract.
- Keep setup concise with small fixtures. Accept some duplication rather than introducing unnecessary test abstractions.
- Remove redundant coverage. Push back on review suggestions that add assertions without protecting meaningful behavior.
Name tests *.unit.test.ts, *.integration.test.ts, or *.e2e.test.ts; colocate them with source.
Use tests/ for shared helpers. For CLI commands, unit-test complex pure logic, integration-test
handlers and feature matrices with realistic Effect layers, and reserve E2E for one to three
golden-path subprocess workflows. Handler integration is the default for command behavior. Use
@effect/vitest's it.live with stateful mock factories returning { layer, state };
assert resulting state and user-visible behavior, not vi.fn() call details. See
login.integration.test.ts and
login.e2e.test.ts; E2E uses
tests/helpers/cli.ts and runSupabase().
Keep tests flake-resistant:
- Subscribe before triggering a transition; use observable readiness/completion, never sleeps or polling delays for propagation, startup, cancellation, cleanup, or port release. Timeouts are guards; use TestClock or fake timers for timing semantics.
- Assume file-level parallelism: use unique IDs, roots, process markers, and derived resources; never disable parallelism globally.
- Never release and reuse an ephemeral port or assume a released endpoint is a dead backend; own a refusal listener or inject the failure.
- Require subprocess readiness and stdout/stderr diagnostics; clean up only exact owned resources. Reproduce and stress flake fixes, then repeat the green case.
Add instructions only for recurring mistakes or non-obvious repository constraints. Update an existing rule before adding another; prefer links to maintained sources over copied examples.