Skip to content
Merged
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
10 changes: 10 additions & 0 deletions apps/cli/docs/stack-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ For temporary selection, set `SUPABASE_EXPERIMENTAL_STACK=1` to select the new b
precedence over `experimental.stack`; an unset or empty value falls back to the file setting.
Other values are rejected. The override is applied before reading the project configuration.

`supabase services` follows the same backend selection. In stack mode it lists image versions and
canonical `ghcr.io/supabase/cli/...` names from the installed CLI's artifact catalog, including
Mailpit and Vector. PostgreSQL uses the selected major version (15 or 17); invalid configuration
or an unsupported PostgreSQL major warns with the cause and uses catalog defaults. This inventory describes the
CLI catalog, not running containers, downloaded images, or service health. The Docker and native
stack runtimes use the same catalog versions, though a running stack launched by another CLI
version or using mirrored images may differ. Legacy version pins and slim-image overrides do not
apply in stack mode. Linked versions remain best-effort; catalog mismatches cannot be changed with
`supabase link`.

`SUPABASE_EXPERIMENTAL_STACK=1 supabase init` writes `[experimental] stack = true` into the new
project config and omits the Docker-era default ports so the stack is not pinned to them.
Without the environment variable, `init` still writes the established template with those ports
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/command-internal/stack-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const STACK_BACKEND_COMMANDS = new Set([
"pull",
"storage",
"seed",
"services",
]);

const isFunctionsServePath = (path: ReadonlyArray<string>): boolean =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ stack = true
expect(yield* resolve({ args: ["inspect", "db"], cwd: root, env: {} })).toBe("stack");
expect(yield* resolve({ args: ["storage", "ls"], cwd: root, env: {} })).toBe("stack");
expect(yield* resolve({ args: ["seed", "buckets"], cwd: root, env: {} })).toBe("stack");
expect(yield* resolve({ args: ["services"], cwd: root, env: {} })).toBe("stack");
}),
);
});
Expand Down
36 changes: 21 additions & 15 deletions apps/cli/src/commands/services/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

## Files Read

| Path | Format | When |
| ---------------------------- | ---------- | ------------------------------------------------------------------------------------------ |
| `supabase/.temp/project-ref` | plain text | when the checkout is linked and no explicit ref is already loaded |
| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` is unset and keyring access falls back to the home token file |
| Path | Format | When |
| ---------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------ |
| `supabase/.temp/project-ref` | plain text | when the checkout is linked and no explicit ref is already loaded |
| `supabase/config.toml` and project environment files | TOML / dotenv | legacy service overrides, linked remote config, or stack PostgreSQL major selection |
| Docker client context/config metadata | JSON | when the shared local project context resolves the Docker hostname; no daemon connection is made |
| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` is unset and keyring access falls back to the home token file |

## Files Written

Expand Down Expand Up @@ -40,18 +42,20 @@ Tenant calls send `apikey: <serviceKey>` and additionally

## Environment Variables

| Variable | Purpose | Required? |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `SUPABASE_ACCESS_TOKEN` | auth token for Management API linked-version checks | no (falls back to keyring, then `~/.supabase/access-token`) |
| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) |
| `SUPABASE_USE_SLIM_IMAGES` | Ambient `process.env` only (`true`/`1` enable). Rewrites current-pin `SERVICE IMAGE`/`name` fields to `ghcr.io/supabase/cli/<service>`. Kong stays on docker.io. Majors 13/15 list the slim `15.14.1.167` pin when the flag is on; flag-off keeps `15.8.1.085`. | no |
| Variable | Purpose | Required? |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `SUPABASE_ACCESS_TOKEN` | auth token for Management API linked-version checks | no (falls back to keyring, then `~/.supabase/access-token`) |
| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) |
| `SUPABASE_USE_SLIM_IMAGES` | Ambient `process.env` only (`true`/`1` enable). Rewrites legacy current-pin `SERVICE IMAGE`/`name` fields to `ghcr.io/supabase/cli/<service>`; Kong stays on docker.io. Majors 13/15 list the slim `15.14.1.167` pin when enabled; flag-off keeps `15.8.1.085`. Ignored by the stack backend. | no |
| `SUPABASE_EXPERIMENTAL_STACK` | Selects the stack backend (`1`) or legacy backend (`0`); unset or empty uses `experimental.stack`. Other values fail routing. | no |
| `SUPABASE_DB_MAJOR_VERSION` | Stack backend PostgreSQL major override; supported values are 15 and 17. | no |

## Exit Codes

| Code | Condition |
| ---- | ------------------------------------------------------------------------------ |
| `0` | success; always prints the local service matrix and optionally linked versions |
| `1` | `--output env` is requested; explicitly unsupported |
| Code | Condition |
| ---- | ------------------------------------------------------------------------------------ |
| `0` | success; always prints the local service matrix and optionally linked versions |
| `1` | `--output env` is unsupported, or `SUPABASE_EXPERIMENTAL_STACK` has an invalid value |

## Output

Expand Down Expand Up @@ -81,8 +85,10 @@ TS-only NDJSON success event with the same `{ services: [...] }` payload.

## Notes

- Local versions come from the command's baked-in service matrix; the command does not inspect Docker state or local config files.
- Backend selection follows canonical experimental-feature routing: `SUPABASE_EXPERIMENTAL_STACK=1|0` takes precedence over `experimental.stack`; unset or empty uses project config, and an invalid environment value fails. Output fields and serializers stay the same.
- The legacy backend uses its baked-in service matrix and honors its existing config/version overrides. The stack backend lists services from the installed CLI artifact catalog using canonical `ghcr.io/supabase/cli/...` image names and catalog versions. Native and Docker runtimes use the same catalog versions. Since the catalog belongs to the installed CLI, an older launched CLI or a newer/mirrored stack image may differ from this inventory. The command does not inspect running containers, image pulls, service health, or live stack state.
- For stack mode, PostgreSQL uses the configured major version or `SUPABASE_DB_MAJOR_VERSION` (15 or 17). Invalid configuration or an unsupported PostgreSQL major warns with the cause and falls back to default catalog versions; absent config uses defaults. Legacy image pins, slim-image rewriting, and remote image overrides do not affect stack results.
- Linked-version checks are best-effort. Remote lookup failures do not change the exit code; they only leave the `LINKED` column empty for unavailable services.
- A malformed linked ref is the one lookup failure that prints an explicit stderr warning (see API Routes above); every other remote failure (network error, expired token, etc.) still fails silently and just leaves `LINKED` empty. Most real-world malformed refs come from an untrimmed `SUPABASE_PROJECT_ID` env var (e.g. a trailing newline from a secrets manager or `.env` file) rather than actual file tampering — the env var is read raw and unlike the on-disk `project-ref` file is never trimmed.
- Version mismatches are reported to stderr as a warning.
- Version mismatches are reported to stderr as a warning. In stack mode, the warning explains that catalog versions cannot be changed with `supabase link`.
- `telemetry.json` is written on every invocation, including `--output env` failures.
60 changes: 60 additions & 0 deletions apps/cli/src/commands/services/services-local-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
artifactServiceKinds,
postgresVersion,
resolveArtifact,
} from "@supabase/stack/internal/service-catalog";
import { Effect, Result } from "effect";
import { loadLocalProjectContext } from "../../command-internal/local-project-context.ts";
import { envOverrideMajorVersion } from "../../command-internal/local-config-values.ts";
import type { ServiceVersionRow } from "../../shared/services/services.shared.ts";
import type { RemoteServiceName } from "../../shared/services/services.shared.ts";

const remoteNames: Readonly<Record<string, RemoteServiceName | undefined>> = {
database: "postgres",
auth: "auth",
rest: "postgrest",
storage: "storage",
};

export const stackServiceVersions = Effect.fn("services.stackServiceVersions")(function* (
workdir: string,
remote: Partial<Record<RemoteServiceName, string>> = {},
) {
const context = yield* loadLocalProjectContext(workdir, (message) => message).pipe(Effect.result);
let configError: string | undefined;
let major: number | undefined;
if (Result.isFailure(context)) configError = context.failure;
else {
const resolvedMajor = yield* Effect.try({
try: () => {
const value = envOverrideMajorVersion(
context.success.config.db.major_version,
context.success.projectEnvValues,
);
if (value !== 15 && value !== 17)
throw new Error(`unsupported PostgreSQL major version: ${value}`);
Comment thread
jgoux marked this conversation as resolved.
Resolved
return value;
},
catch: (cause) => (cause instanceof Error ? cause.message : String(cause)),
}).pipe(Effect.result);
if (Result.isFailure(resolvedMajor)) configError = resolvedMajor.failure;
else major = resolvedMajor.success;
}
return yield* Effect.forEach(artifactServiceKinds(), (service) =>
Effect.gen(function* () {
const artifact = yield* resolveArtifact({
service,
...(service === "database" && major !== undefined
? { version: postgresVersion(String(major)) }
: {}),
});
const name = artifact.image.split("@")[0]?.replace(/:[^/:]+$/, "") ?? artifact.image;
const remoteName = remoteNames[service];
return {
name,
local: artifact.version,
remote: remoteName === undefined ? "" : (remote[remoteName] ?? ""),
} satisfies ServiceVersionRow;
}),
).pipe(Effect.map((rows) => ({ rows, configError })));
});
Loading
Loading