Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
25a02e7
feat(stack): report redacted configuration drift
jgoux Sep 7, 2026
ce02a21
feat(cli): add experimental stack status
jgoux Sep 7, 2026
77f0924
fix(cli): classify experimental stack status errors
jgoux Sep 7, 2026
903da61
fix(cli): refine experimental stack status reporting
jgoux Sep 7, 2026
a9bc9b0
test(cli): cover experimental stack status output parity
jgoux Sep 7, 2026
251fa8a
fix(cli): clarify experimental stack status
jgoux Sep 7, 2026
5fad357
fix(cli): clarify missing stack status guidance
jgoux Sep 7, 2026
f8ef0e7
chore(cli): annotate stack status fixtures
jgoux Sep 8, 2026
276a3f9
test(cli): complete stack status service mocks
jgoux Sep 8, 2026
82144ea
test(cli): scope status mock to available stack methods
jgoux Sep 8, 2026
9ad64f6
fix(cli): retarget stack status onto current stack command APIs
avallete Sep 11, 2026
e5eb53b
Merge remote-tracking branch 'origin/develop' into feat/experimental-…
Coly010 Sep 11, 2026
4b54642
fix(stack): return database credentials when Auth is disabled
Coly010 Sep 11, 2026
3a78e84
chore(cli): repair stack status quality checks
Coly010 Sep 11, 2026
ad51305
feat(cli): add stack status --env export
Coly010 Sep 11, 2026
51f94f4
feat(cli): route top-level status through the stack feature flag
Coly010 Sep 11, 2026
138cf64
fix(cli): emit the stack status env map without a message key
Coly010 Sep 11, 2026
5fbf3eb
Merge remote-tracking branch 'origin/develop' into feat/experimental-…
Coly010 Sep 12, 2026
6b21e43
chore(docs): regenerate config schemas for stack status routing
Coly010 Sep 12, 2026
f4f708b
fix(cli): address stack status review findings
Coly010 Sep 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 37 additions & 16 deletions apps/cli/docs/stack-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,40 @@
command interface may change, and it is excluded from the CLI compatibility promise. It is
available regardless of the project's backend setting and supports both Docker and native runtimes.

| Command | Purpose |
| ------------------------ | ------------------------------------------ |
| `supabase stack start` | Create or resume the project's stack. |
| `supabase stack destroy` | Permanently delete one stack and its data. |
| `supabase stack stop` | Stop a stack while retaining its data. |
| Command | Purpose |
| ------------------------ | --------------------------------------------------------------------------------- |
| `supabase stack start` | Create or resume the project's stack. |
| `supabase stack status` | Show identity, readiness, and drift, or export connection variables with `--env`. |
| `supabase stack stop` | Stop a stack while retaining its data. |
| `supabase stack destroy` | Permanently delete one stack and its data. |

Use each command's `--help` for its available targeting and runtime options.

## Exporting environment variables

```sh
supabase stack status --env --output-format text > .env.local
supabase status --env --override-name API_URL=NEXT_PUBLIC_SUPABASE_URL,ANON_KEY=NEXT_PUBLIC_SUPABASE_ANON_KEY
supabase stack status --env --output-format json
```

The top-level example requires the stack backend flag described below. `--env` exports the
connection URLs and credentials of the running stack; text mode emits dotenv assignments, and JSON
or stream-JSON mode emits a variable map. Add `--output-format text` for an explicit dotenv file
regardless of automatic agent output detection; this is dotenv data, not a shell script. Only this
explicit export reveals credentials. Ordinary status remains free of secrets. `--override-name`
accepts repeated or comma-separated `EXPORTED_VARIABLE=NAME` entries, requires `--env`, and rejects
unknown variables, invalid names, and collisions. API credentials are omitted when Auth is disabled.

The stack backend rejects every explicit legacy `-o/--output` value: `env`, `pretty`, `json`,
`toml`, `yaml`, `table`, and `csv`. `--output-format text`, `json`, or `stream-json` replace them.
`-o env` becomes `--env`.

## Selecting the top-level commands

The top-level `supabase start` and `supabase stop` commands use the legacy backend by default.
To make them aliases of the corresponding `supabase stack` commands, add this to
`supabase/config.toml`:
The top-level `supabase start`, `supabase stop`, and `supabase status` commands use the legacy
backend by default. To make them aliases of the corresponding `supabase stack` commands, add this
to `supabase/config.toml`:

```toml
[experimental]
Expand All @@ -25,22 +46,22 @@ stack = true

The selected backend determines accepted flags, help, and completion before the command is parsed.
Set the flag to `false`, or remove it, to restore the legacy top-level commands. Explicit
`supabase stack` commands always use the new backend. `supabase status` always uses its existing
command implementation and is unaffected by this flag.
`supabase stack` commands always use the new backend; `supabase status` is routed the same way as
`supabase start` and `supabase stop`.

Root help and root completion do not read project configuration, so they remain available without
a project directory. Help and completion for `start` and `stop` resolve the same backend as the
command itself. If the project configuration cannot be read or parsed, or if
a project directory. Help and completion for `start`, `status`, and `stop` resolve the same backend
as the command itself. If the project configuration cannot be read or parsed, or if
`experimental.stack` has an invalid value, routing falls back to the legacy backend. An invalid
`SUPABASE_EXPERIMENTAL_STACK` value is still an error; set it to `0` to select the legacy
top-level command explicitly, or use the explicit `supabase stack start` or `supabase stack stop`
command.
top-level command explicitly, or use the explicit `supabase stack start`, `supabase stack status`,
or `supabase stack stop` command.

For temporary selection, set `SUPABASE_EXPERIMENTAL_STACK=1` to select the new backend or
`SUPABASE_EXPERIMENTAL_STACK=0` to select the legacy backend. This environment variable takes
precedence over `experimental.stack`; an unset or empty value falls back to the file setting.
Other values are rejected. The override affects only the top-level lifecycle aliases and is
applied before reading the project configuration.
Other values are rejected. The override affects only the top-level `start`, `status`, and `stop`
aliases and is applied before reading the project configuration.

## Data and configuration

Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/cli/complete.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1267,7 +1267,7 @@ describe("tryComplete", () => {
expect(stderrWrites).toHaveLength(1);
expect(stderrWrites[0]).toContain("SUPABASE_EXPERIMENTAL_STACK must be 0 or 1 when set");
expect(stderrWrites[0]).toContain(
"Suggestion: Set SUPABASE_EXPERIMENTAL_STACK=0 to use legacy start/stop, or use `supabase stack`.",
"Suggestion: Set SUPABASE_EXPERIMENTAL_STACK=0 to use legacy start/stop/status, or use `supabase stack`.",
);
expect(exits).toEqual([1]);
});
Expand Down
7 changes: 6 additions & 1 deletion apps/cli/src/cli/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { encryptionCommand } from "../commands/encryption/encryption.command.ts"
import { stackRuntimeLayer, stackCommand } from "../commands/experimental/stack/stack.command.ts";
import { stackStartCommand } from "../commands/experimental/stack/start/start.command.ts";
import { stackStopCommand } from "../commands/experimental/stack/stop/stop.command.ts";
import { stackStatusCommand } from "../commands/experimental/stack/status/status.command.ts";
import type { StackBackend } from "../commands/experimental/stack/stack-backend.ts";
import { computeCommand } from "../commands/experimental/compute/compute.command.ts";
import { feedbackCommand } from "../commands/feedback/feedback.command.ts";
Expand Down Expand Up @@ -76,6 +77,10 @@ export const stackStopAliasCommand = stackStopCommand.pipe(
Command.provide(commandRuntimeLayer(["stop"])),
Command.provide(stackRuntimeLayer),
);
const stackStatusAliasCommand = stackStatusCommand.pipe(
Command.provide(commandRuntimeLayer(["status"])),
Command.provide(stackRuntimeLayer),
);

export const rootCommandForFeatures = (
options: {
Expand Down Expand Up @@ -119,7 +124,7 @@ export const rootCommandForFeatures = (
ssoCommand,
stackCommand,
options.stackBackend === "stack" ? stackStartAliasCommand : startCommand,
statusCommand,
options.stackBackend === "stack" ? stackStatusAliasCommand : statusCommand,
options.stackBackend === "stack" ? stackStopAliasCommand : stopCommand,
storageCommand,
telemetryCommand,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe("resolveStackBackend", () => {
}),
);

it.effect("selects the configured backend for top-level start and stop", () => {
it.effect("selects the configured backend for top-level start, stop, and status", () => {
const root = project(`project_id = "stack-routing-test"
[api]
port = 55421
Expand All @@ -50,7 +50,7 @@ stack = true
return Effect.gen(function* () {
expect(yield* resolve({ args: ["start"], cwd: join(root, "nested"), env: {} })).toBe("stack");
expect(yield* resolve({ args: ["stop"], cwd: root, env: {} })).toBe("stack");
expect(yield* resolve({ args: ["status"], cwd: root, env: {} })).toBe("legacy");
expect(yield* resolve({ args: ["status"], cwd: root, env: {} })).toBe("stack");
}).pipe(Effect.ensuring(Effect.sync(() => rmSync(root, { recursive: true, force: true }))));
});

Expand Down Expand Up @@ -215,14 +215,14 @@ stack = true
expect(completionFlags("stack", "start")).not.toContain("--ignore-health-check");
});

it("keeps status and stack on their existing command trees", () => {
it("routes status like start and stop, and keeps stack on its own command tree", () => {
for (const backend of ["legacy", "stack"] as const) {
const stackCommands = respondToComplete(rootCommandForFeatures({ stackBackend: backend }), [
"__complete",
"stack",
"",
])?.candidates.map(({ name }) => name);
expect(stackCommands).toEqual(["destroy", "start", "stop"]);
expect(stackCommands).toEqual(["destroy", "start", "status", "stop"]);

expect(completionFlags(backend, "status")).toContain("--override-name");
}
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/commands/experimental/stack/stack-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class StackRoutingError extends Data.TaggedError("StackRoutingError")<{
readonly cause?: unknown;
}> {
get suggestion(): string {
return "Set SUPABASE_EXPERIMENTAL_STACK=0 to use legacy start/stop, or use `supabase stack`.";
return "Set SUPABASE_EXPERIMENTAL_STACK=0 to use legacy start/stop/status, or use `supabase stack`.";
}

get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
Expand Down Expand Up @@ -92,7 +92,7 @@ export const resolveStackBackend = (input: {
// The explicit namespace is always backed by the stack runtime and does
// not need a project config or environment lookup to select it.
if (command === "stack") return "stack";
if (command !== "start" && command !== "stop") return "legacy";
if (command !== "start" && command !== "stop" && command !== "status") return "legacy";

const configValue = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down
11 changes: 10 additions & 1 deletion apps/cli/src/commands/experimental/stack/stack.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { debugLoggerLayer } from "../../../command-internal/debug-logger.layer.t
import { telemetryStateLayer } from "../../../telemetry/telemetry-state.layer.ts";
import { stackStartCommand as stackStartCommandBase } from "./start/start.command.ts";
import { stackStopCommand as stackStopCommandBase } from "./stop/stop.command.ts";
import { stackStatusCommand as stackStatusCommandBase } from "./status/status.command.ts";
import { stackDestroyCommand as stackDestroyCommandBase } from "./destroy/destroy.command.ts";
import { stackApiLayer, stackTargetResolverLayer } from "./stack.shared.ts";

Expand All @@ -22,6 +23,9 @@ const stackStartCommand = stackStartCommandBase.pipe(
const stackStopCommand = stackStopCommandBase.pipe(
Command.provide(commandRuntimeLayer(["stack", "stop"])),
);
const stackStatusCommand = stackStatusCommandBase.pipe(
Command.provide(commandRuntimeLayer(["stack", "status"])),
);
const stackDestroyCommand = stackDestroyCommandBase.pipe(
Command.provide(commandRuntimeLayer(["stack", "destroy"])),
);
Expand All @@ -31,6 +35,11 @@ export const stackCommand = Command.make("stack").pipe(
"Manage an experimental, unstable local Supabase stack with the new backend. This command is excluded from the CLI compatibility promise.",
),
Command.withShortDescription("Manage experimental local stacks"),
Command.withSubcommands([stackStartCommand, stackStopCommand, stackDestroyCommand]),
Command.withSubcommands([
stackStartCommand,
stackStatusCommand,
stackStopCommand,
stackDestroyCommand,
]),
Command.provide(stackRuntimeLayer),
);
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Starts and stops a native stack through the compiled CLI binary, then uses the package's
// public Promise API to inspect and destroy that stack.
// Starts a native stack through the compiled CLI binary, checks its status and connection-variable
// export, stops it, checks status again, then uses the package's public Promise API to inspect and
// destroy that stack.
// oxlint-disable-next-line effecttsgo/process-env -- package runtime composition is scoped below.

// oxlint-disable-next-line effecttsgo/node-builtin-import -- compiled CLI fixture requires host process/filesystem APIs
Expand All @@ -9,6 +10,7 @@ import { execFile as execFileCallback } from "node:child_process";
// oxlint-disable-next-line effecttsgo/node-builtin-import -- compiled CLI fixture requires host process/filesystem APIs
import path from "node:path";
import { promisify } from "node:util";
import { parse as parseDotenv } from "dotenv";
import { afterEach, describe, expect, test } from "vitest";
import { makeTempHome, runSupabase } from "../../../../../tests/helpers/cli.ts";

Expand Down Expand Up @@ -172,6 +174,34 @@ describe("stack start (compiled e2e)", () => {
const databasePath = path.join(homeDir.dir, "managed", "stacks", idText, "data", "database");
await access(path.join(databasePath, "PG_VERSION"));

const status = await runSupabase(["stack", "status", "--stack-id", idText], {
cwd: projectRoot,
home: homeDir.dir,
exitTimeoutMs: CLEANUP_TIMEOUT_MS,
});
expect(status.exitCode, `stdout:\n${status.stdout}\nstderr:\n${status.stderr}`).toBe(0);
expect(status.stdout).toContain(`(${idText})`);
expect(status.stdout).toContain("Owner: running");
expect(status.stdout).toContain("Lifecycle: running");
expect(status.stdout).toContain("Readiness: ready");
expect(status.stdout).toMatch(/Config drift: (changed|unchanged)/u);

const env = await runSupabase(
["stack", "status", "--env", "--stack-id", idText, "--output-format", "json"],
{ cwd: projectRoot, home: homeDir.dir, exitTimeoutMs: CLEANUP_TIMEOUT_MS },
);
expect(env.exitCode, `stdout:\n${env.stdout}\nstderr:\n${env.stderr}`).toBe(0);
const variables = JSON.parse(env.stdout) as Record<string, string>;
expect(Object.keys(variables)).toEqual(["DB_URL"]);
expect(variables.DB_URL).toMatch(/^postgresql:\/\/postgres:.+@.+:\d+\/postgres$/u);

const dotenv = await runSupabase(
["stack", "status", "--env", "--stack-id", idText, "--output-format", "text"],
{ cwd: projectRoot, home: homeDir.dir, exitTimeoutMs: CLEANUP_TIMEOUT_MS },
);
expect(dotenv.exitCode, `stdout:\n${dotenv.stdout}\nstderr:\n${dotenv.stderr}`).toBe(0);
expect(parseDotenv(dotenv.stdout)).toEqual(variables);

await rm(path.join(projectRoot, "supabase", "config.toml"));
const stop = await runSupabase(["stack", "stop", "--stack-id", idText], {
cwd: projectRoot,
Expand All @@ -187,6 +217,28 @@ describe("stack start (compiled e2e)", () => {
expect(observed.lifecycle).toBe("stopped");
expect(observed.database).toBe("stopped");

const stoppedStatus = await runSupabase(["stack", "status", "--stack-id", idText], {
cwd: projectRoot,
home: homeDir.dir,
exitTimeoutMs: CLEANUP_TIMEOUT_MS,
});
expect(
stoppedStatus.exitCode,
`stdout:\n${stoppedStatus.stdout}\nstderr:\n${stoppedStatus.stderr}`,
).toBe(0);
expect(stoppedStatus.stdout).toContain("Owner: absent");
expect(stoppedStatus.stdout).toContain("Lifecycle: unavailable");
expect(stoppedStatus.stdout).toContain("Readiness: unknown");

const stoppedEnv = await runSupabase(["stack", "status", "--env", "--stack-id", idText], {
cwd: projectRoot,
home: homeDir.dir,
exitTimeoutMs: CLEANUP_TIMEOUT_MS,
});
expect(stoppedEnv.exitCode).not.toBe(0);
expect(stoppedEnv.stdout).not.toContain("DB_URL");
expect(stoppedEnv.stderr).toContain("must be running");

await access(path.join(databasePath, "PG_VERSION"));

await destroyStack(homeDir.dir, idText);
Expand Down
Loading