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
68197f4
feat(stack): publish native service catalog capabilities
jgoux Aug 28, 2026
a77f0e2
test(stack): remove obsolete native rejection case
jgoux Aug 28, 2026
ed1974b
fix(stack): normalize native catalog release tags
jgoux Aug 28, 2026
b483f9c
feat(stack): journal native service logs
jgoux Aug 28, 2026
8b0fede
fix(stack): harden native log journaling
jgoux Aug 28, 2026
550b146
fix(stack): recover journal rotation failures
jgoux Aug 28, 2026
add4219
fix(stack): align catalog versions with frozen releases
jgoux Aug 28, 2026
6650ea8
fix(stack): use portable native service launchers
jgoux Aug 28, 2026
c5a3255
feat(stack): add native auxiliary service definitions
jgoux Aug 28, 2026
fa88751
fix(stack): bind native Studio to loopback
jgoux Aug 28, 2026
ece8b8d
test(stack): preserve Docker Studio bind host
jgoux Aug 28, 2026
4f16f13
feat(stack): add native storage image proxy services
jgoux Aug 28, 2026
885989b
fix(stack): remove unused native image proxy path
jgoux Aug 28, 2026
1ca2651
feat(stack): add native analytics vector and pooler
jgoux Aug 28, 2026
177e6ba
test(stack): stabilize native log integration checks
jgoux Aug 28, 2026
c82b432
feat(stack): build complete native service graph
jgoux Aug 28, 2026
7a34002
fix(stack): use native Realtime HTTP health check
jgoux Aug 28, 2026
0bdb9c4
test(stack): qualify native graph lifecycle recovery
jgoux Aug 28, 2026
1a81406
test(stack): qualify native service graph journey
jgoux Aug 28, 2026
8f3ed4b
chore(stack): keep catalog metadata internal
jgoux Aug 28, 2026
3901930
fix(stack): isolate native graph runtime metadata
jgoux Aug 29, 2026
f4f9c41
fix(stack): harden native service lifecycle
jgoux Aug 29, 2026
cb6eb4a
fix(stack): preserve runtime-specific service versions
jgoux Aug 29, 2026
7d78ad8
fix(stack): preserve native listener and port boundaries
jgoux Aug 29, 2026
5587f64
fix(stack): complete native service graph qualification
jgoux Aug 31, 2026
a36346a
test(stack): align Pooler Docker default
jgoux Aug 31, 2026
459a251
fix(stack): preserve current Docker service versions
jgoux Aug 31, 2026
246d59d
fix(stack): harden native service lifecycle
jgoux Aug 31, 2026
9f3f0fb
fix(stack): accept static musl service manifests
jgoux Aug 31, 2026
45588eb
fix(stack): load Edge config from the module graph
jgoux Aug 31, 2026
e2bc5e7
test(stack): align lazy Realtime deadline
jgoux Aug 31, 2026
dfe88c3
test(stack): extend lazy Realtime heartbeat window
jgoux Aug 31, 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=${DOCKER_DEFAULT_VERSIONS.pooler}.`,
});
});
});
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
Loading
Loading