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
197 changes: 156 additions & 41 deletions apps/cli/src/command-internal/stack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,142 @@ const envNestedPortOrConfigured = (
return typeof nested?.[key] === "number" ? configured : undefined;
};

/** A setting's `config.toml` key and the `SUPABASE_*` env var that overrides it. */
export interface StackEndpointSetting {
readonly configPath: string;
readonly envVar: string;
}

/**
* A port setting's `config.toml` key and env var, shared by `createCreations` and
* `stackEndpointSetting` so both report the same names; a new endpoint still needs an entry in
* `endpointSettingsByServiceEndpoint`.
*/
interface PortSetting extends StackEndpointSetting {
readonly section: string;
readonly nestedSection?: string;
readonly key: string;
}

const DB_PORT: PortSetting = {
envVar: "SUPABASE_DB_PORT",
section: "db",
key: "port",
configPath: "db.port",
};
const API_PORT: PortSetting = {
envVar: "SUPABASE_API_PORT",
section: "api",
key: "port",
configPath: "api.port",
};
const STUDIO_PORT: PortSetting = {
envVar: "SUPABASE_STUDIO_PORT",
section: "studio",
key: "port",
configPath: "studio.port",
};
const DB_POOLER_PORT: PortSetting = {
envVar: "SUPABASE_DB_POOLER_PORT",
section: "db",
nestedSection: "pooler",
key: "port",
configPath: "db.pooler.port",
};
const LOCAL_SMTP_PORT: PortSetting = {
envVar: "SUPABASE_LOCAL_SMTP_PORT",
section: "local_smtp",
key: "port",
configPath: "local_smtp.port",
};
const LOCAL_SMTP_SMTP_PORT: PortSetting = {
envVar: "SUPABASE_LOCAL_SMTP_SMTP_PORT",
section: "local_smtp",
key: "smtp_port",
configPath: "local_smtp.smtp_port",
};
const LOCAL_SMTP_POP3_PORT: PortSetting = {
envVar: "SUPABASE_LOCAL_SMTP_POP3_PORT",
section: "local_smtp",
key: "pop3_port",
configPath: "local_smtp.pop3_port",
};
const ANALYTICS_PORT: PortSetting = {
envVar: "SUPABASE_ANALYTICS_PORT",
section: "analytics",
key: "port",
configPath: "analytics.port",
};
const ANALYTICS_VECTOR_PORT: PortSetting = {
envVar: "SUPABASE_ANALYTICS_VECTOR_PORT",
section: "analytics",
key: "vector_port",
configPath: "analytics.vector_port",
};
const EDGE_RUNTIME_INSPECTOR_PORT: PortSetting = {
envVar: "SUPABASE_EDGE_RUNTIME_INSPECTOR_PORT",
section: "edge_runtime",
key: "inspector_port",
configPath: "edge_runtime.inspector_port",
};

/** Resolves one `PortSetting` against the loaded document and env, picking the nested variant when needed. */
const resolvePort = (
setting: PortSetting,
document: Readonly<Record<string, unknown>> | undefined,
configured: number,
env: Readonly<Record<string, string>>,
): number | undefined =>
setting.nestedSection === undefined
? envPortOrConfigured(setting.envVar, document, setting.section, setting.key, configured, env)
: envNestedPortOrConfigured(
setting.envVar,
document,
setting.section,
setting.nestedSection,
setting.key,
configured,
env,
);

/**
* Maps a saved stack endpoint (service + endpoint name) to the `PortSetting` that controls it.
* An endpoint missing here (e.g. `pooler.http`, `realtime.rpc`) is always automatic.
*/
const endpointSettingsByServiceEndpoint: Readonly<Record<string, StackEndpointSetting>> = {
"database.sql": DB_PORT,
"pooler.sql": DB_POOLER_PORT,
"analytics.http": ANALYTICS_PORT,
"vector.http": ANALYTICS_VECTOR_PORT,
"studio.http": STUDIO_PORT,
"mail.http": LOCAL_SMTP_PORT,
"mail.smtp": LOCAL_SMTP_SMTP_PORT,
"mail.pop3": LOCAL_SMTP_POP3_PORT,
"functions.inspector": EDGE_RUNTIME_INSPECTOR_PORT,
"rest.http": API_PORT,
"auth.http": API_PORT,
"realtime.http": API_PORT,
"storage.http": API_PORT,
"functions.http": API_PORT,
};

/** The config.toml key and env var override for a service endpoint, when the CLI exposes one. */
export const stackEndpointSetting = (
service: string,
endpoint: string,
): StackEndpointSetting | undefined => endpointSettingsByServiceEndpoint[`${service}.${endpoint}`];

/**
* `db.major_version`'s config key and `SUPABASE_DB_MAJOR_VERSION` override. Unlike the ports
* above, `envOverrideMajorVersion` (shared with `db-bootstrap` and the legacy local stack) hardcodes
* its own name/field, so there is no single call site to read this from without widening that
* shared helper's signature; the two literals here are kept in sync by hand.
*/
export const stackMajorVersionSetting: StackEndpointSetting = {
configPath: "db.major_version",
envVar: "SUPABASE_DB_MAJOR_VERSION",
};

const authProviderNames = [
"apple",
"azure",
Expand Down Expand Up @@ -1064,68 +1200,51 @@ export const loadStackConfig = Effect.fn("StackConfig.load")(
},
catch: (cause) => new StackConfigError({ message: String(cause) }),
});
const dbPort = envPortOrConfigured(
"SUPABASE_DB_PORT",
const dbPort = resolvePort(
DB_PORT,
document,
"db",
"port",
validatedConfig.db.port,
context.projectEnvValues,
);
const apiPort = envPortOrConfigured(
"SUPABASE_API_PORT",
const apiPort = resolvePort(
API_PORT,
document,
"api",
"port",
validatedConfig.api.port,
context.projectEnvValues,
);
const studioPort = envPortOrConfigured(
"SUPABASE_STUDIO_PORT",
const studioPort = resolvePort(
STUDIO_PORT,
document,
"studio",
"port",
validatedConfig.studio.port,
context.projectEnvValues,
);
const poolerPort = envNestedPortOrConfigured(
"SUPABASE_DB_POOLER_PORT",
const poolerPort = resolvePort(
DB_POOLER_PORT,
document,
"db",
"pooler",
"port",
validatedConfig.db.pooler.port,
context.projectEnvValues,
);
const mailPort = envPortOrConfigured(
"SUPABASE_LOCAL_SMTP_PORT",
const mailPort = resolvePort(
LOCAL_SMTP_PORT,
document,
"local_smtp",
"port",
validatedConfig.local_smtp.port,
context.projectEnvValues,
);
const mailSmtpPort = envPortOrConfigured(
"SUPABASE_LOCAL_SMTP_SMTP_PORT",
const mailSmtpPort = resolvePort(
LOCAL_SMTP_SMTP_PORT,
document,
"local_smtp",
"smtp_port",
validatedConfig.local_smtp.smtp_port ?? 0,
context.projectEnvValues,
);
const mailPop3Port = envPortOrConfigured(
"SUPABASE_LOCAL_SMTP_POP3_PORT",
const mailPop3Port = resolvePort(
LOCAL_SMTP_POP3_PORT,
document,
"local_smtp",
"pop3_port",
validatedConfig.local_smtp.pop3_port ?? 0,
context.projectEnvValues,
);
const analyticsPort = envPortOrConfigured(
"SUPABASE_ANALYTICS_PORT",
const analyticsPort = resolvePort(
ANALYTICS_PORT,
document,
"analytics",
"port",
validatedConfig.analytics.port,
context.projectEnvValues,
);
Expand Down Expand Up @@ -1257,11 +1376,9 @@ export const loadStackConfig = Effect.fn("StackConfig.load")(
config: { apiKey: "api-key" },
endpoints: {
http: endpoint(
envPortOrConfigured(
"SUPABASE_ANALYTICS_VECTOR_PORT",
resolvePort(
ANALYTICS_VECTOR_PORT,
document,
"analytics",
"vector_port",
validatedConfig.analytics.vector_port ?? 0,
context.projectEnvValues,
),
Expand Down Expand Up @@ -1295,11 +1412,9 @@ export const loadStackConfig = Effect.fn("StackConfig.load")(
endpoints: {
http: endpoint(apiPort),
inspector: endpoint(
envPortOrConfigured(
"SUPABASE_EDGE_RUNTIME_INSPECTOR_PORT",
resolvePort(
EDGE_RUNTIME_INSPECTOR_PORT,
document,
"edge_runtime",
"inspector_port",
validatedConfig.edge_runtime.inspector_port,
context.projectEnvValues,
),
Expand Down
14 changes: 12 additions & 2 deletions apps/cli/src/commands/experimental/stack/start/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ and ports are retained. Changed exclusions reuse existing service identities, da
Removed services remain saved and stopped so including them again can reuse them; a saved stopped
instance of a newly included service is reused when its endpoints and versions still match. The
project configuration file is unchanged. A changed endpoint, artifact version, or PostgreSQL major
version fails before modifying the stopped composition, naming the changed setting and suggesting
`supabase stack destroy` to recreate the stack.
version fails before modifying the stopped composition, naming the `config.toml` key or
`SUPABASE_*` env var behind the change with its saved and requested values, and suggesting either
reverting it or running the stack's exact `supabase stack destroy` command to recreate it.

## First startup and retries

Expand Down Expand Up @@ -137,3 +138,12 @@ name (protocol, address, port, and URL, matching `stack status`, with no synthet
empty message. See [`docs/stack-commands.md`](../../../../../docs/stack-commands.md) for an
example. Failures retain typed command errors and package diagnostics. Telemetry state is flushed
after success or failure.

A rejected configuration change additionally carries `stack_changes` on the JSON/stream-json error
envelope: one entry per affected service (a shared setting such as the API port appears once per
API-backed service, unlike the deduplicated text message), each with `service`, `path` (the
composition planner's dotted path, e.g. `endpoints.http.port`, not a `config.toml` key), `key`,
`saved`, `requested`, and `editable`. `recreate_command` is the exact `supabase stack destroy
--stack-id <id>` invocation, without `--yes`, since destroy deletes local database data; running it
non-interactively or with `--output-format json`/`--output-format stream-json` requires passing
`--yes` explicitly.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Layer } from "effect";
import { Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";
import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { machineErrorContextLayer } from "../../../../shared/output/machine-error-context.layer.ts";
import { withCommandTelemetry } from "../../../../telemetry/command-telemetry.ts";
import { stringSliceFlag } from "../../../../command-internal/string-slice-flag.ts";
import { stdinLayer } from "../../../../shared/runtime/stdin.layer.ts";
Expand Down Expand Up @@ -66,5 +68,7 @@ export const stackStartCommand = Command.make("start", config).pipe(
),
// `stackStart`'s bucket-seeding path satisfies `promptYesNo`'s `Stdin` requirement here even
// though it always passes `yes: true`/`interactive: false` and never reaches the prompt.
Command.provide(stdinLayer),
// `machineErrorContextLayer` carries the structured incompatible-change payload onto the
// JSON/stream-json error envelope.
Command.provide(Layer.mergeAll(stdinLayer, machineErrorContextLayer)),
);
Loading
Loading