Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
735ea29
test(stack): qualify complete slim Docker graph
jgoux Aug 28, 2026
4f3129a
Merge remote-tracking branch 'origin/develop' into juliengoux/cli-211…
jgoux Aug 28, 2026
4683e0d
test(stack): satisfy Effect lint in Docker journey
jgoux Aug 28, 2026
6e332dd
test(stack): budget complete Docker journey
jgoux Aug 28, 2026
f24be1b
test(stack): qualify native core services
jgoux Aug 28, 2026
d17f9bf
test(stack): isolate native Auth session journey
jgoux Aug 28, 2026
41be775
fix(stack): preserve lazy services across repeated stop
jgoux Aug 28, 2026
ccbf5c5
fix(stack): retain stopped companion allowances
jgoux Aug 28, 2026
0bd4a77
feat(stack): publish native service catalog capabilities
jgoux Aug 28, 2026
5afc247
test(stack): remove obsolete native rejection case
jgoux Aug 28, 2026
41788a5
fix(stack): normalize native catalog release tags
jgoux Aug 28, 2026
41b8782
feat(stack): journal native service logs
jgoux Aug 28, 2026
56c65d5
fix(stack): harden native log journaling
jgoux Aug 28, 2026
3b4cdd7
fix(stack): recover journal rotation failures
jgoux Aug 28, 2026
37c2627
fix(stack): align catalog versions with frozen releases
jgoux Aug 28, 2026
5a5b780
fix(stack): use portable native service launchers
jgoux Aug 28, 2026
c50cbbc
feat(stack): add native auxiliary service definitions
jgoux Aug 28, 2026
249fffa
fix(stack): bind native Studio to loopback
jgoux Aug 28, 2026
cda4ccf
test(stack): preserve Docker Studio bind host
jgoux Aug 28, 2026
156a6c1
feat(stack): add native storage image proxy services
jgoux Aug 28, 2026
391f704
fix(stack): remove unused native image proxy path
jgoux Aug 28, 2026
70cfbf4
feat(stack): add native analytics vector and pooler
jgoux Aug 28, 2026
d38ded2
test(stack): stabilize native log integration checks
jgoux Aug 28, 2026
409a776
feat(stack): build complete native service graph
jgoux Aug 28, 2026
53dc437
fix(stack): use native Realtime HTTP health check
jgoux Aug 28, 2026
ae34c8b
test(stack): qualify native graph lifecycle recovery
jgoux Aug 28, 2026
b7830c8
test(stack): qualify native service graph journey
jgoux Aug 28, 2026
d1515d4
chore(stack): keep catalog metadata internal
jgoux Aug 28, 2026
c912c31
fix(stack): isolate native graph runtime metadata
jgoux Aug 29, 2026
3a0834d
fix(stack): harden native service lifecycle
jgoux Aug 29, 2026
5fe089a
fix(stack): preserve runtime-specific service versions
jgoux Aug 29, 2026
69acdd0
fix(stack): preserve native listener and port boundaries
jgoux Aug 29, 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
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const startFullStack = Effect.fnUntraced(function* (opts: FunctionsDevStackOptio
yield* output.info("No local stack is running. Starting the local Supabase stack...");
yield* ensureProjectStateIgnored(cliProjectHome.projectRoot);

const serviceVersionContext = yield* resolveServiceVersionContext([], undefined);
const serviceVersionContext = yield* resolveServiceVersionContext([], undefined, "docker");
const loadedCliConfig = yield* loadCliConfig(cliProjectHome.projectRoot);
const stackConfig = {
...withServiceVersions(toStartStackConfig([], "docker"), serviceVersionContext.runtimeVersions),
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/next/commands/link/link.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const link = Effect.fnUntraced(function* (flags: LinkFlags) {
projectDir: cliProjectHome.projectRoot,
})).map((stack) => ({
stackName: stack.name,
services: fillServiceVersionManifest(stack.versions),
services: fillServiceVersionManifest(stack.versions, stack.launch.mode),
Comment thread
jgoux marked this conversation as resolved.
})),
);
const linkedProject = refreshed.linkedProject;
Expand Down
11 changes: 5 additions & 6 deletions apps/cli/src/next/commands/services/services.handler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
fillServiceVersionManifest,
planStackVersions,
resolveStackSummary,
} from "@supabase/stack/effect";
import { planStackVersions, resolveStackSummary } from "@supabase/stack/effect";
import { Effect, Exit, Option } from "effect";
import { Credentials } from "../../auth/credentials.service.ts";
import { CliSettings } from "../../config/cli-settings.service.ts";
Expand Down Expand Up @@ -47,7 +43,10 @@ export const services = Effect.fnUntraced(function* () {
const serviceVersionContext = planStackVersions({
...(Option.isSome(linkedState) ? { candidateBaseline: linkedState.value.versions } : {}),
...(Option.isSome(existingSummary)
? { pinnedBaseline: fillServiceVersionManifest(existingSummary.value.versions) }
? {
pinnedBaseline: existingSummary.value.versions,
runtime: existingSummary.value.launch.mode,
}
: {}),
Comment thread
jgoux marked this conversation as resolved.
...(Option.isSome(localServiceVersions)
? { localOverrides: localServiceVersions.value.versions }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DEFAULT_VERSIONS } from "@supabase/stack/effect";
import { DEFAULT_VERSIONS, DOCKER_DEFAULT_VERSIONS } from "@supabase/stack/effect";
import { describe, expect, test } from "vitest";
import { Effect, Layer } from "effect";
import {
Expand Down Expand Up @@ -80,4 +80,29 @@ describe("service version overrides", () => {
updateFingerprint: undefined,
});
});

test("uses Docker defaults and tags when explicitly resolving Docker versions", async () => {
await expect(
Effect.runPromise(
resolveServiceVersionContext(["pooler=v2.9.7"], undefined, "docker").pipe(
Effect.provide(
Layer.mergeAll(mockProjectLinkState(), mockCliProjectLocalServiceVersions()),
),
),
),
).resolves.toMatchObject({
candidateBaseline: DOCKER_DEFAULT_VERSIONS,
pinnedBaseline: DOCKER_DEFAULT_VERSIONS,
runtimeVersions: { ...DOCKER_DEFAULT_VERSIONS, pooler: "2.9.7" },
activeOverrides: [{ service: "pooler", version: "2.9.7", source: "flag" }],
});
});

test("suggests the Docker default for an empty Docker override", async () => {
await expect(
Effect.runPromise(parseServiceVersionOverrides(["pooler="], "docker").pipe(Effect.flip)),
).resolves.toMatchObject({
suggestion: "Pass --service-version pooler=2.9.7.",
});
});
});
12 changes: 6 additions & 6 deletions apps/cli/src/next/commands/start/start.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
DEFAULT_MANAGED_STACK_NAME,
daemonLayer,
restartManagedStackForUpgrade,
fillServiceVersionManifest,
resolveStackSummary,
selectStackRuntime,
type StackSummary,
} from "@supabase/stack/effect";
import { Command, Flag } from "effect/unstable/cli";
Expand Down Expand Up @@ -178,17 +178,17 @@ export const startCommand = Command.make("start", flags).pipe(
cwd: runtimeInfo.cwd,
name: flags.stack,
}).pipe(Effect.catchTag("NoRunningStackError", () => Effect.succeed(undefined)));
const requestedMode = flags.mode ?? existingSummary?.launch.mode;
const effectiveMode = requestedMode ?? (yield* selectStackRuntime()).mode;
const serviceVersionContext = yield* resolveServiceVersionContext(
flags.serviceVersion,
existingSummary === undefined
? undefined
: fillServiceVersionManifest(existingSummary.versions),
existingSummary?.versions,
effectiveMode,
);
const loadedCliConfig = yield* loadCliConfig(cliProjectHome.projectRoot);
// Tri-state in config.toml: unset and explicit `true` both auto-expose new entities in
// `public` (the cloud default); only explicit `false` revokes the default Data API GRANTs.
const autoExposeNewTables = loadedCliConfig?.config.api.auto_expose_new_tables ?? true;
const effectiveMode = flags.mode ?? existingSummary?.launch.mode;
const baseStackConfig = withServiceVersions(
toStartStackConfig(flags.exclude, effectiveMode),
serviceVersionContext.runtimeVersions,
Expand All @@ -212,7 +212,7 @@ export const startCommand = Command.make("start", flags).pipe(
portDocument: portIntents,
});
const launch = {
...(flags.mode === undefined ? {} : { mode: flags.mode }),
mode: effectiveMode,
versions: serviceVersionContext.pinnedBaseline,
excludedServices: flags.exclude,
...(existingSummary?.lastNotifiedUpdateFingerprint === undefined
Expand Down
5 changes: 3 additions & 2 deletions apps/cli/src/next/commands/start/start.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from "@effect/vitest";
import { BunServices } from "@effect/platform-bun";
import {
DEFAULT_VERSIONS,
DOCKER_DEFAULT_VERSIONS,
connectLayer,
fillServiceVersionManifest,
planStackVersions,
Expand Down Expand Up @@ -34,8 +34,9 @@ describe("start handler", () => {
Effect.provide(fixture.baseLayer),
Effect.flatMap((stackLayer) => {
const out = mockOutput({ interactive: false });
const versions = fillServiceVersionManifest(DEFAULT_VERSIONS);
const versions = fillServiceVersionManifest(DOCKER_DEFAULT_VERSIONS, "docker");
const serviceVersionContext = planStackVersions({
runtime: "docker",
pinnedBaseline: versions,
candidateBaseline: versions,
});
Expand Down
7 changes: 4 additions & 3 deletions apps/cli/src/next/commands/status/status.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Context, Effect, Layer, Option, Predicate } from "effect";
import { loadCliConfig } from "@supabase/config/effect";
import {
connectLayer,
fillServiceVersionManifest,
resolveStackSummary,
Stack,
type StackSummary,
Expand Down Expand Up @@ -183,7 +182,8 @@ export const status = Effect.fnUntraced(function* (_flags: StatusFlags) {
const message = "Local Supabase stack is stopped.";
const serviceVersionContext = yield* resolveServiceVersionContext(
[],
fillServiceVersionManifest(summary.value.versions),
summary.value.versions,
summary.value.launch.mode,
);
const data = {
stack: summary.value.name,
Expand Down Expand Up @@ -241,7 +241,8 @@ export const status = Effect.fnUntraced(function* (_flags: StatusFlags) {
const { info, services } = stackResult;
const serviceVersionContext = yield* resolveServiceVersionContext(
[],
fillServiceVersionManifest(summary.versions),
summary.versions,
summary.launch.mode,
);
const sortedServices = [...services].sort((a, b) => a.name.localeCompare(b.name));
const allReady = services.every((service) =>
Expand Down
8 changes: 6 additions & 2 deletions apps/cli/src/next/commands/update/update.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const update = Effect.fnUntraced(function* (flags: UpdateFlags) {
projectDir: cliProjectHome.projectRoot,
})).map((stack) => ({
stackName: stack.name,
services: fillServiceVersionManifest(stack.versions),
services: fillServiceVersionManifest(stack.versions, stack.launch.mode),
})),
);
const changedVersions = diffCachedLinkedVersions(
Expand Down Expand Up @@ -99,7 +99,11 @@ export const update = Effect.fnUntraced(function* (flags: UpdateFlags) {
[],
Option.match(existingSummary, {
onNone: () => undefined,
onSome: (summary) => fillServiceVersionManifest(summary.versions),
onSome: (summary) => summary.versions,
}),
Option.match(existingSummary, {
onNone: () => undefined,
onSome: (summary) => summary.launch.mode,
}),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BunServices } from "@effect/platform-bun";
import { Effect, Layer } from "effect";
import { rmSync } from "node:fs";
import { join } from "node:path";
import { DEFAULT_VERSIONS } from "@supabase/stack/effect";
import { DOCKER_DEFAULT_VERSIONS } from "@supabase/stack/effect";
import { update } from "./update.handler.ts";
import {
mockOutput,
Expand Down Expand Up @@ -34,7 +34,7 @@ describe("update handler", () => {
Effect.tap(
Effect.promise(async () => {
const document = await fixture.readDocument();
expect(document?.launch?.versions).toEqual(DEFAULT_VERSIONS);
expect(document?.launch?.versions).toEqual(DOCKER_DEFAULT_VERSIONS);
}),
),
Effect.ensuring(Effect.promise(() => fixture.dispose())),
Expand Down
35 changes: 34 additions & 1 deletion apps/cli/src/next/config/managed-port-intents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
import type { LoadedCliConfig } from "@supabase/config";
import { PORT_CATALOG, PORT_FIELDS, portFieldsForConfigInput } from "@supabase/stack/effect";

const hasOwn = (value: object, key: string): boolean =>
Object.prototype.hasOwnProperty.call(value, key);

const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null;

/**
* The CLI's public config still calls the selected pooler listener `port`.
* The stack catalog splits that listener by pool mode, so project documents
* need a derived key before the managed port planner resolves its intents.
*/
const withPoolerPortIntent = (document: Readonly<Record<string, unknown>>) => {
const db = document.db;
if (!isRecord(db)) return document;
const pooler = db.pooler;
if (!isRecord(pooler) || !hasOwn(pooler, "port") || typeof pooler.port !== "number") {
return document;
}

const key = pooler.pool_mode === "session" ? "session_port" : "transaction_port";
return {
...document,
db: {
...db,
pooler: {
...pooler,
[key]: pooler.port,
},
},
};
};

/**
* Preserve the raw project document alongside the resolved stack config so
* explicit sticky ports remain distinct from omitted automatic ports.
Expand All @@ -13,9 +45,10 @@ export const managedPortIntents = (
const disabledFields = PORT_FIELDS.filter(
(field) => PORT_CATALOG[field].persistence === "sticky" && !activeFields.includes(field),
);
const document = withPoolerPortIntent(loadedCliConfig?.document ?? {});
return {
activeFields,
disabledFields,
document: loadedCliConfig?.document ?? {},
document,
};
};
32 changes: 32 additions & 0 deletions apps/cli/src/next/config/managed-port-intents.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { managedPortIntents } from "./managed-port-intents.ts";

const stackConfig = {
mode: "docker",
pooler: {},
} satisfies Parameters<typeof managedPortIntents>[0];

describe("managedPortIntents", () => {
it("maps the configured pooler port to the transaction listener by default", () => {
expect(
managedPortIntents(stackConfig, {
document: { db: { pooler: { port: 6543 } } },
}).document,
).toEqual({ db: { pooler: { port: 6543, transaction_port: 6543 } } });
});

it("maps the configured pooler port to the session listener when requested", () => {
expect(
managedPortIntents(stackConfig, {
document: { db: { pooler: { port: 6544, pool_mode: "session" } } },
}).document,
).toEqual({
db: { pooler: { port: 6544, pool_mode: "session", session_port: 6544 } },
});
});

it("leaves the document unchanged when the pooler port is absent", () => {
const document = { db: { pooler: { pool_mode: "session" } } };
expect(managedPortIntents(stackConfig, { document }).document).toBe(document);
});
});
15 changes: 9 additions & 6 deletions apps/cli/src/next/config/service-version-resolution.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ServiceName, StackVersionPlan, VersionManifest } from "@supabase/stack/effect";
import type { ServiceName, StackVersionPlan, VersionRuntime } from "@supabase/stack/effect";
import {
DEFAULT_VERSIONS,
defaultVersionsForRuntime,
normalizeServiceVersion,
planStackVersions,
SERVICE_NAMES,
Expand Down Expand Up @@ -33,6 +33,7 @@ function isServiceName(value: string): value is ServiceName {

export const parseServiceVersionOverrides = Effect.fnUntraced(function* (
rawOverrides: ReadonlyArray<string>,
runtime: VersionRuntime = "native",
) {
const overrides: Partial<Record<ServiceName, string>> = {};

Expand All @@ -55,29 +56,31 @@ export const parseServiceVersionOverrides = Effect.fnUntraced(function* (
return yield* Effect.fail(
new InvalidServiceVersionOverrideError({
detail: `Invalid service version override '${rawOverride}'. Expected format service=version.`,
suggestion: `Pass --service-version ${rawService}=${DEFAULT_VERSIONS[rawService]}.`,
suggestion: `Pass --service-version ${rawService}=${defaultVersionsForRuntime(runtime)[rawService]}.`,
}),
);
}

overrides[rawService] = normalizeServiceVersion(rawService, rawVersion);
overrides[rawService] = normalizeServiceVersion(rawService, rawVersion, runtime);
}

return overrides;
});

export const resolveServiceVersionContext = Effect.fnUntraced(function* (
rawOverrides: ReadonlyArray<string>,
pinnedBaselineOverride?: VersionManifest,
pinnedBaselineOverride?: Partial<Record<ServiceName, string | undefined>>,
runtime?: VersionRuntime,
) {
const projectLinkState = yield* ProjectLinkState;
const cliProjectLocalServiceVersions = yield* CliProjectLocalServiceVersions;

const flagOverrides = yield* parseServiceVersionOverrides(rawOverrides);
const flagOverrides = yield* parseServiceVersionOverrides(rawOverrides, runtime);
const localState = yield* cliProjectLocalServiceVersions.load;
const linkedState = yield* projectLinkState.load;

return planStackVersions({
runtime,
candidateBaseline: Option.match(linkedState, {
onNone: () => undefined,
onSome: (state) => state.versions,
Expand Down
4 changes: 3 additions & 1 deletion packages/process-compose/src/HealthProbe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const executeProbe = (
> => {
return Match.valueTags(probe, {
Http: (probe) =>
HttpClient.get(`${probe.scheme}://${probe.host}:${probe.port}${probe.path}`).pipe(
HttpClient.get(`${probe.scheme}://${probe.host}:${probe.port}${probe.path}`, {
headers: probe.headers,
}).pipe(
Effect.timeout(Duration.seconds(timeoutSeconds)),
Effect.map((res) => res.status >= 200 && res.status < 300),
Effect.orElseSucceed(() => false),
Expand Down
Loading