Skip to content
Open
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
3 changes: 3 additions & 0 deletions .oxlintrc.effect.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"!apps/cli/src/commands/vanity-subdomains/**",
"!apps/cli/src/commands/whoami/**",
"!apps/cli/src/shared/compute/**",
"!apps/cli/src/shared/functions/deploy.ts",
"!apps/cli/src/shared/functions/serve.ts",
"!apps/cli/src/shared/functions/serve-main-bundler.ts",
// Last match wins across the whole list: keep bare re-exclusions after every
// `!` entry that would otherwise re-include them.
"apps/cli/src/shared/compute/stacks/**",
Expand Down
26 changes: 18 additions & 8 deletions apps/cli/docs/stack-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ regardless of automatic agent output detection; this is dotenv data, not a shell
are quoted so that sourcing the file performs no shell expansion. Only this
explicit export reveals credentials. Ordinary status remains free of secrets. `--override-name`
accepts repeated or comma-separated `EXPORTED_VARIABLE=NAME` entries, requires `--env`, and rejects
unknown variables, invalid names, and collisions. API credentials are omitted when Auth is disabled.
unknown variables, invalid names, and collisions. API credentials belong to the stack and remain
available when Auth is disabled; database credentials require a registered default database.

The stack backend rejects every explicit legacy `-o/--output` value: `env`, `pretty`, `json`,
`toml`, `yaml`, `table`, and `csv`. `--output-format text`, `json`, or `stream-json` replace them.
Expand Down Expand Up @@ -88,27 +89,36 @@ precedence over `experimental.stack`; an unset or empty value falls back to the
Other values are rejected. The override is applied before reading the project configuration.

When the flag is on, `--local` targets of the `db`, `migration`, `test db`, `gen types`, and
`inspect` families use the project stack and provision throwaway shadow Postgres through
`@supabase/stack` (`EphemeralPostgres`). Top-level `supabase pull` uses the same stack shadow
`inspect` families use the project stack and register independent shadow PostgreSQL instances
through `@supabase/stack`. Top-level `supabase pull` uses the same stack shadow
as `db pull`. Linked and `--db-url` targets stay on the Management API for engine selection.
A `--db-url` that matches `config.toml` host and port is still rewritten like a published
stack target for dump's tool container. Compose names (`supabase_db_*`, `supabase_network_*`,
`db:5432`) are not used. The stack backend requires the in-process pg-delta engine;
`--use-migra`, `--use-pgadmin`, `--use-pg-schema`, and `db pull --diff-engine migra` are
rejected. The flag does not switch the `functions` command family.
rejected. Functions serve uses the shared Functions instance and can start without PostgreSQL or
Auth. It restarts that instance for changed configuration or watched source files and leaves it
running when the CLI exits. Storage commands and bucket seeding use the same stack selection when
the flag is enabled; see [Storage and bucket seeding](#storage-and-bucket-seeding) below.

`storage ls`/`cp`/`mv`/`rm` and `seed buckets` (including bucket seeding inside `db reset
--local`) also consult `experimental.stack`, with the same `SUPABASE_EXPERIMENTAL_STACK`
env-precedence rule as `start`/`stop`/`status`. See
[Storage and bucket seeding](#storage-and-bucket-seeding) below. Explicit `--linked`/
`--project-ref` remote targeting for these commands is unaffected by the flag either way.

Functions paths declared in `config.toml` retain their `supabase/`-relative base, including
configured entrypoints, import maps, and static files outside `supabase/functions`. The stack
resolves their dependencies and mounts source files read-only for container runtimes. These inputs
remain available after the serving CLI exits.

`db start` brings up a postgres-only project stack on first create. An existing stack resumes
its persisted services (webhooks setup only; no second overlay or migrate-and-seed).
`supabase start` while that postgres-only stack is running stops it and starts the full
configured stack, keeping data. `--from-backup` is not supported on the stack path. `db reset
--local` and declarative `--apply` wipe Postgres through `resetDatabase` and then migrate or
seed on stack credentials.
Whole-stack startup uses registered instances and preserves their IDs and planned endpoints.
`--from-backup` is not supported on the stack path. `db reset --local` and declarative `--apply`
rebuild the primary database through CLI SQL orchestration using a fresh registered baseline,
then migrate or seed through managed stack credentials. The primary instance keeps its identity;
unrelated service data is retained.

`gen types --local` and `inspect db … --local` resolve the project stack through the same
`--local` database target as `db dump`. They do not start a stack.
Expand Down
11 changes: 9 additions & 2 deletions apps/cli/scripts/build-binary.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { bundleServeMainTemplate } from "../src/shared/functions/serve-main-bundler.ts";
import { Effect } from "effect";
import { bundleServeMainTemplate as bundleCliServeMainTemplate } from "../src/shared/functions/serve-main-bundler.ts";
import { bundleServeMainTemplate as bundleStackServeMainTemplate } from "../../../packages/stack/src/functions/serve-main-bundler.ts";
import { OXFMT_OPTIONAL_PLUGIN_EXTERNALS } from "./bundle-externals.ts";

/**
Expand All @@ -22,7 +24,12 @@ const result = await Bun.build({
external: [...OXFMT_OPTIONAL_PLUGIN_EXTERNALS],
define: {
SUPABASE_CLI_VERSION: JSON.stringify(packageJson.version),
SUPABASE_FUNCTIONS_SERVE_MAIN_TEMPLATE: JSON.stringify(await bundleServeMainTemplate()),
SUPABASE_FUNCTIONS_SERVE_MAIN_TEMPLATE: JSON.stringify(
await Effect.runPromise(bundleCliServeMainTemplate),
),
SUPABASE_STACK_FUNCTIONS_SERVE_MAIN_TEMPLATE: JSON.stringify(
await Effect.runPromise(bundleStackServeMainTemplate),
),
},
});
for (const log of result.logs) {
Expand Down
11 changes: 9 additions & 2 deletions apps/cli/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { parseArgs } from "node:util";
import { bundleServeMainTemplate } from "../src/shared/functions/serve-main-bundler.ts";
import { Effect } from "effect";
import { bundleServeMainTemplate as bundleCliServeMainTemplate } from "../src/shared/functions/serve-main-bundler.ts";
import { bundleServeMainTemplate as bundleStackServeMainTemplate } from "../../../packages/stack/src/functions/serve-main-bundler.ts";
import { OXFMT_OPTIONAL_PLUGIN_EXTERNALS } from "./bundle-externals.ts";
import { darwinBinaries, MACOS_IDENTIFIERS } from "./macos-signing.ts";

Expand Down Expand Up @@ -88,7 +90,12 @@ const entrypoint = path.join(root, "apps/cli/src/main.ts");
const distDir = path.join(root, "dist");
const goSource = path.resolve(root, "apps/cli-go");
const buildDefines = {
SUPABASE_FUNCTIONS_SERVE_MAIN_TEMPLATE: JSON.stringify(await bundleServeMainTemplate()),
SUPABASE_FUNCTIONS_SERVE_MAIN_TEMPLATE: JSON.stringify(
await Effect.runPromise(bundleCliServeMainTemplate),
),
SUPABASE_STACK_FUNCTIONS_SERVE_MAIN_TEMPLATE: JSON.stringify(
await Effect.runPromise(bundleStackServeMainTemplate),
),
"process.env.SUPABASE_CLI_POSTHOG_KEY": JSON.stringify(process.env.POSTHOG_API_KEY ?? ""),
"process.env.SUPABASE_CLI_POSTHOG_HOST": JSON.stringify(process.env.POSTHOG_ENDPOINT ?? ""),
};
Expand Down
28 changes: 13 additions & 15 deletions apps/cli/src/command-internal/db-bootstrap/container-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
containerArchiveBytes,
isUserDefinedDockerNetwork,
} from "../../shared/functions/functions-docker.ts";
import { FunctionsDockerError } from "../../shared/functions/functions-docker.errors.ts";
import {
buildStartContainerCreateArgs,
applyBitbucketStartContainerFilter,
Expand Down Expand Up @@ -610,21 +611,18 @@ function copyStartSecretFilesIntoContainer(
): Effect.Effect<void, ContainerCreateError> {
if (secretFiles.length === 0) return Effect.void;

return Effect.tryPromise({
try: () =>
containerArchiveBytes(
Object.fromEntries(
secretFiles.map((secretFile) => [secretFile.containerPath, secretFile.content]),
),
),
catch: (cause) =>
new ContainerCreateError({
message: `failed to create docker container: failed to prepare container secret files: ${
cause instanceof Error ? cause.message : String(cause)
}`,
reason: "internal",
}),
}).pipe(
return containerArchiveBytes(
Object.fromEntries(
secretFiles.map((secretFile) => [secretFile.containerPath, secretFile.content]),
),
).pipe(
Effect.mapError(
(cause: FunctionsDockerError) =>
new ContainerCreateError({
message: `failed to create docker container: failed to prepare container secret files: ${cause.message}`,
reason: "internal",
}),
),
Effect.flatMap((archive) =>
dockerCopyArchiveIntoContainer(spawner, archive, `${containerId}:/`, secretCopyFailure),
),
Expand Down
11 changes: 5 additions & 6 deletions apps/cli/src/command-internal/db-bootstrap/pgdata-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* can't afford downtime.
*/

import { randomUUID } from "node:crypto";
import { Effect, Option, Stream, type FileSystem } from "effect";
import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";

Expand Down Expand Up @@ -137,20 +138,18 @@ export const stampPgDataBaselineMarker = (
* Streams `docker cp <containerId>:${PGDATA_PATH} -` to a temp file next to `tarPath` and
* `rename`s it into place, so a partially written tar is never visible under the final name;
* any failure removes the temp file. The container must already be stopped (callers own the
* stop/start), and the temp name is scoped by pid alone, so concurrent exports to the same
* `tarPath` must be externally serialized (`shadow-cache.ts` holds `shadowExportMutex`).
* stop/start), and each invocation gets its own UUID-scoped temp name.
*/
export const exportPgDataTar = (
spawner: Spawner,
containerId: string,
fs: FileSystem.FileSystem,
tarPath: string,
): Effect.Effect<void, PgDataSnapshotUnavailable> => {
const tempPath = `${tarPath}.${process.pid}.partial`;
const tempPath = `${tarPath}.${randomUUID()}.partial`;
return Effect.gen(function* () {
// Clears a leftover temp file (crashed predecessor or pre-created by another process) so the
// exclusive-create below starts from a fresh inode.
yield* fs.remove(tempPath).pipe(Effect.orElseSucceed(() => undefined));
// The UUID path is absent by construction; O_EXCL below also protects against the vanishingly
// unlikely collision without removing another export's in-flight file.
yield* Effect.scoped(
Effect.gen(function* () {
const child = yield* spawnContainerCli(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ const RESET_RECREATE_DATABASES_STATEMENTS = [
* statements. Roles are not dropped here since they are cluster-level entities — use stop then
* start instead.
*/
const resetRecreateDatabases = Effect.fnUntraced(function* (session: DbSession) {
/** Recreates the two project databases in an existing managed Postgres instance. */
export const resetRecreateDatabases = Effect.fnUntraced(function* (session: DbSession) {
yield* resetDisconnectClients(session);
for (const [index, statement] of RESET_RECREATE_DATABASES_STATEMENTS.entries()) {
yield* session.exec(statement).pipe(
Expand Down
Loading
Loading