Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 42 additions & 0 deletions apps/cli/docs/stack-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
| 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. |
| `supabase stack status` | Inspect stack state and endpoints. |
| `supabase stack list` | List registered stacks. |
Expand Down Expand Up @@ -34,3 +35,44 @@ This flag currently selects only the `start`, `stop`, and `status` aliases. It d
The backends own separate state and databases. Enabling the flag does not import, copy, seed from, or reuse the legacy database, and does not stop a running legacy stack. Normal project migrations and seed configuration are separate from importing legacy database data.

The flag is local CLI configuration in `supabase/config.toml` and is excluded from hosted project configuration. When the environment override is absent or empty, lifecycle routing reads that exact file after applying the CLI’s working-directory rules, including `--workdir` and `SUPABASE_WORKDIR`; a JSON-only project does not enable the flag. Selecting a backend does not bypass validation when the selected command later loads its full configuration.

## Service selection and shutdown

`supabase stack start --exclude studio,analytics -x mail` disables those services
in the effective start configuration. Valid names are `rest`, `auth`, `realtime`,
`storage`, `functions`, `studio`, `mail`, `analytics`, and `pooler`; `database` is
required. The project file is unchanged. The effective configuration is retained
in stack state, so stop the stack and start without `--exclude` to restore the
project’s configured services. `--eager` waits for enabled services to become ready.

`supabase stack stop --all` stops every readable stack in the new backend’s registry and
preserves data. It cannot be combined with `--stack` or `--stack-id`. All discovered stops are
attempted; unreadable or unsupported entries produce warnings and are skipped. If any entry is skipped or a stop fails,
the command exits nonzero with stopped, failed, and skipped counts. A registry-root enumeration failure prevents
any stack from being stopped.

`supabase stack destroy --stack feature-a` permanently removes exactly that
stack and its data after confirmation. Use `--yes` for unattended execution.
There is no top-level `destroy` alias and no bulk destroy option.

## 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 backend flag. `--env` exports URLs and credentials
from a running stack: `DB_URL`, `API_URL`, `ANON_KEY`, `SERVICE_ROLE_KEY`,
`PUBLISHABLE_KEY`, `SECRET_KEY`, and available `STUDIO_URL`, `INBUCKET_URL`,
`S3_PROTOCOL_ACCESS_KEY_ID`, `S3_PROTOCOL_ACCESS_KEY_SECRET`, `S3_PROTOCOL_REGION`,
and `S3_PROTOCOL_URL`. API credentials are omitted when Auth is disabled; optional endpoints and S3
credentials are omitted when unavailable.

Text mode emits dotenv assignments; JSON and stream-JSON modes emit a variable
map. For an explicit dotenv file regardless of automatic agent output detection,
add `--output-format text`. This is dotenv data, not a shell script to execute.
Only this explicit export reveals credentials; ordinary status remains secret-free.
`--override-name` accepts repeated or comma-separated `EXPORTED_VARIABLE=NAME`
entries, requires `--env`, and rejects unknown variables, invalid names, and collisions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# `supabase stack destroy`

Permanently stops and removes one managed new-backend stack, including its persisted data.

The command targets the current project stack by default, or an explicit `--stack` name or
`--stack-id`. It requires interactive confirmation; `--yes` is required for non-interactive and
machine-readable invocations. It never accepts `--all`.

The stack package reads the selected descriptor and removes its resources and
state under `${SUPABASE_HOME:-~/.supabase}/managed/stacks/<id>`. It owns stopping
the supervisor, removing native processes or containers, and deleting the
stack’s persistent data. The CLI does not delete paths or Docker resources
itself and makes no Management API calls. Project files are retained.

The normal CLI settings select the working directory and stack home.
`SUPABASE_YES` participates in the existing confirmation setting; explicit
`--yes=false` overrides it. The prompt identifies the name, project directory,
and immutable stack ID. Rejection or missing noninteractive confirmation
performs no destructive operation.

Text output reports the destroyed stack ID. JSON and stream-JSON return
`{ "destroyed": true, "id": "..." }`. Success exits 0; invalid targets,
confirmation refusal, and destruction failures exit 1. Standard command
instrumentation records command metadata without exporting credentials.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Command, Flag } from "effect/unstable/cli";
import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { withLegacyCommandInstrumentation } from "../../../../telemetry/legacy-command-instrumentation.ts";
import { legacyExperimentalStackDestroy } from "./destroy.handler.ts";

const config = {
stack: Flag.string("stack").pipe(
Flag.withDescription(
"Destroy the stack with this name (defaults to the current project stack).",
),
Flag.optional,
),
stackId: Flag.string("stack-id").pipe(
Flag.withDescription("Destroy an existing stack by id."),
Flag.optional,
),
} as const;

export const legacyExperimentalStackDestroyCommand = Command.make("destroy", config).pipe(
Command.withDescription("Permanently destroy a managed local Supabase stack and its data."),
Command.withShortDescription("Destroy a managed local stack"),
Command.withExamples([
{
command: "supabase stack destroy --stack feature-a --yes",
description: "Permanently destroy the feature-a stack",
},
]),
Command.withHandler((flags) =>
legacyExperimentalStackDestroy(flags).pipe(
withLegacyCommandInstrumentation({ flags, config }),
withJsonErrorHandling,
),
),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Data } from "effect";
import {
actionability,
type CliErrorActionabilityDeclaration,
ErrorActionabilityId,
} from "../../../../shared/telemetry/error-actionability.ts";

export class LegacyExperimentalStackDestroyError extends Data.TaggedError(
"LegacyExperimentalStackDestroyError",
)<{
readonly reason: "flags" | "confirmation" | "invalid-config" | "lifecycle" | "unknown";
readonly message: string;
readonly suggestion?: string;
readonly cause?: unknown;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
switch (this.reason) {
case "flags":
case "confirmation":
return actionability.provideFlags;
case "invalid-config":
case "lifecycle":
return actionability.invalidConfig;
case "unknown":
return actionability.unknown;
}
return actionability.unknown;
}
}
141 changes: 141 additions & 0 deletions apps/cli/src/commands/experimental/stack/destroy/destroy.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Effect, Match, Option } from "effect";
import { isStackError, isStackId, StackIdSchema } from "@supabase/stack/effect";
import { Output } from "../../../../shared/output/output.service.ts";
import { LegacyOutputFlag, legacyResolveYes } from "../../../../shared/legacy/global-flags.ts";
import { legacyPromptYesNo } from "../../../../shared/legacy/legacy-prompt-yes-no.ts";
import { Tty } from "../../../../shared/runtime/tty.service.ts";
import { LegacyCliSettings } from "../../../../config/legacy-cli-settings.service.ts";
import { LegacyExperimentalStackApi } from "../stack.shared.ts";
import { LegacyExperimentalStackDestroyError } from "./destroy.errors.ts";

export interface LegacyExperimentalStackDestroyFlags {
readonly stack: Option.Option<string>;
readonly stackId: Option.Option<string>;
}

export const legacyValidateExperimentalStackDestroyTarget = (
flags: Pick<LegacyExperimentalStackDestroyFlags, "stack" | "stackId">,
) =>
Option.isSome(flags.stack) && Option.isSome(flags.stackId)
? Effect.fail(
new LegacyExperimentalStackDestroyError({
reason: "flags",
message: "--stack and --stack-id cannot be used together",
}),
)
: Effect.void;

const destroyError = (error: unknown): LegacyExperimentalStackDestroyError => {
const stackError = isStackError(error) ? error : undefined;
const reason =
stackError === undefined
? "unknown"
: Match.value(stackError).pipe(
Match.tag("StackNotFoundError", "InvalidStackIdentityError", () => "flags" as const),
Match.tag(
"StackOwnershipConflictError",
"StackNotRunningError",
"StackMustBeStoppedError",
"StackLifecycleConflictError",
"StackRuntimeError",
"StackCleanupError",
"StackDestructionError",
"StackUpgradeRequiredError",
() => "lifecycle" as const,
),
Match.tag(
"InvalidStackConfigError",
"StackStateFormatUnsupportedError",
"InvalidProjectRootError",
"StackStateInvalidError",
() => "invalid-config" as const,
),
Match.orElse(() => "unknown" as const),
);
return new LegacyExperimentalStackDestroyError({
reason,
message: stackError?.message ?? String(error),
cause: error,
});
};

const resolveTarget = Effect.fnUntraced(function* (
flags: LegacyExperimentalStackDestroyFlags,
projectRoot: string,
) {
const api = yield* LegacyExperimentalStackApi;
if (Option.isSome(flags.stackId)) {
const id = flags.stackId.value;
if (!isStackId(id))
return yield* new LegacyExperimentalStackDestroyError({
reason: "flags",
message: "--stack-id must be a lowercase SHA-256 stack id",
});
return yield* api.inspectStack(id).pipe(
Effect.map(({ descriptor }) => descriptor),
Effect.mapError(destroyError),
);
}
const found = yield* api
.findStack({
projectRoot,
...(Option.isSome(flags.stack) ? { name: flags.stack.value } : {}),
})
.pipe(Effect.mapError(destroyError));
if (Option.isNone(found)) {
const label = Option.isSome(flags.stack) ? ` named "${flags.stack.value}"` : "";
return yield* new LegacyExperimentalStackDestroyError({
reason: "flags",
message: `No managed stack${label} was found for this project.`,
suggestion: "Choose an existing --stack name or omit --stack for the current project.",
});
}
return found.value;
});

export const legacyExperimentalStackDestroy = Effect.fn("legacy.experimental.stack.destroy")(
function* (flags: LegacyExperimentalStackDestroyFlags) {
const output = yield* Output;
const settings = yield* LegacyCliSettings;
const api = yield* LegacyExperimentalStackApi;
const legacyOutput = yield* Effect.serviceOption(LegacyOutputFlag);
if (Option.isSome(legacyOutput) && Option.isSome(legacyOutput.value))
return yield* new LegacyExperimentalStackDestroyError({
reason: "flags",
message: "The legacy -o/--output flag is not supported here; use --output-format json.",
suggestion: "Use --output-format json or --output-format text.",
});
yield* legacyValidateExperimentalStackDestroyTarget(flags);
const target = yield* resolveTarget(flags, settings.workdir);
const yes = yield* legacyResolveYes;
const tty = yield* Tty;
if (!yes && (!tty.stdinIsTty || output.format !== "text"))
return yield* new LegacyExperimentalStackDestroyError({
reason: "confirmation",
message: "Destroying a stack requires confirmation; rerun with --yes.",
suggestion: "Pass --yes when running non-interactively or in a machine-readable format.",
});
const confirmed = yield* legacyPromptYesNo(
output,
yes,
`Permanently destroy stack "${target.name}" at ${target.projectRoot} (${target.id}) and all of its data?`,
false,
);
if (!confirmed)
return yield* new LegacyExperimentalStackDestroyError({
reason: "confirmation",
message: "Stack destruction was not confirmed.",
});
const stack = yield* api
.openStack(StackIdSchema.make(target.id))
.pipe(Effect.mapError(destroyError));
const destroying = yield* output.task(`Destroying stack ${target.id}...`);
yield* stack.destroy().pipe(
Effect.tapError((error) => destroying.fail(error.message)),
Effect.tap(() => destroying.clear()),
Effect.mapError(destroyError),
);
if (output.format === "text") yield* output.raw(`Stack ${target.id} destroyed.\n`);
else yield* output.success("", { destroyed: true, id: target.id });
},
);
Loading
Loading