Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
fc38a12
test(cli): codify local stack config parity
jgoux Aug 5, 2026
259d5f1
refactor(cli): centralize local stack launch configuration
jgoux Aug 5, 2026
fb748ae
chore(cli): integrate stack runtime prerequisites
jgoux Aug 5, 2026
311416c
refactor(cli): enforce local stack launch policy
jgoux Aug 5, 2026
5bb3ac5
refactor(cli): map local stack core topology
jgoux Aug 5, 2026
c848aba
feat(stack): implement local auth config parity
jgoux Aug 5, 2026
3e70eb6
test(cli): refine config parity decisions
jgoux Aug 5, 2026
678f920
feat(stack): add database bootstrap phases
jgoux Aug 5, 2026
c5b9152
test(stack): await stopped lifecycle state
jgoux Aug 5, 2026
7b1cb80
feat(stack): implement data-plane config parity
jgoux Aug 5, 2026
fb6e62a
fix(stack): preserve database bootstrap semantics
jgoux Aug 5, 2026
538c71b
fix(cli): make migration discovery gate value-sensitive
jgoux Aug 5, 2026
73392f3
test(cli): tighten parity classifications
jgoux Aug 5, 2026
7274b57
merge: integrate stack factory locality
jgoux Aug 5, 2026
e0dfbf8
refactor(stack): make functions configuration explicit
jgoux Aug 5, 2026
73bcbbb
feat(cli): apply functions config during local start
jgoux Aug 5, 2026
79c2968
refactor(stack): narrow package entrypoints
jgoux Aug 5, 2026
6e2f9c9
merge: join stack export audit ancestry
jgoux Aug 5, 2026
e6d8e8b
test(cli): refine parity presence rules
jgoux Aug 5, 2026
dfa4c5c
refactor(stack): separate function configuration from activation
jgoux Aug 5, 2026
42b28c7
feat(cli): close local stack config parity gaps
jgoux Aug 5, 2026
45437fe
fix(cli): preserve remote precedence for auth bindings
jgoux Aug 5, 2026
1fab5d2
chore(cli): merge current develop
jgoux Aug 5, 2026
3a4c2e1
Merge branch 'agent/stack-package-export-audit' into agent/local-stac…
jgoux Aug 5, 2026
7d9b68d
chore(tooling): remove obsolete knip exceptions
jgoux Aug 5, 2026
03ec30d
merge: join refreshed parity ledger ancestry
jgoux Aug 5, 2026
73bf51b
Merge branch 'agent/stack-package-export-audit' into agent/local-stac…
jgoux Aug 5, 2026
4d3f388
test(cli): refine local config presence semantics
jgoux Aug 5, 2026
65e8657
Merge branch 'agent/stack-package-export-audit' into agent/local-stac…
jgoux Aug 5, 2026
be8b2ad
Merge branch 'agent/local-stack-config-parity-ledger' into agent/loca…
jgoux Aug 5, 2026
cc50c84
fix(cli): honor effective local config presence
jgoux Aug 5, 2026
9469846
fix(cli): classify generated config defaults
jgoux Aug 5, 2026
74c8765
Merge branch 'agent/stack-package-export-audit' into agent/local-stac…
jgoux Aug 5, 2026
a499697
Merge branch 'agent/local-stack-config-parity-ledger' into agent/loca…
jgoux Aug 5, 2026
1c91012
Merge branch 'agent/stack-package-export-audit' into agent/local-stac…
jgoux Aug 5, 2026
4893ea4
Merge branch 'agent/stack-package-export-audit' into agent/local-stac…
jgoux Aug 5, 2026
bf2289a
test(cli): ignore generated stack defaults
jgoux Aug 5, 2026
6198a7f
Merge branch 'agent/stack-package-export-audit' into agent/local-stac…
jgoux Aug 5, 2026
636aaa5
Merge branch 'agent/local-stack-config-parity-ledger' into agent/loca…
jgoux Aug 5, 2026
3f0083e
Merge branch 'agent/stack-package-export-audit' of github.com:supabas…
jgoux Aug 5, 2026
494f3d8
Merge branch 'agent/stack-package-export-audit' of github.com:supabas…
jgoux Aug 5, 2026
70d97da
fix(cli): complete local config parity inventory
jgoux Aug 5, 2026
09fe34a
Merge branch 'agent/local-stack-config-parity-ledger' of github.com:s…
jgoux Aug 5, 2026
5b4ddbe
Merge branch 'agent/stack-package-export-audit' of github.com:supabas…
jgoux Aug 5, 2026
de9db54
test(cli): refine parity presence semantics
jgoux Aug 5, 2026
9a3ca81
Merge branch 'agent/local-stack-config-parity-ledger' of github.com:s…
jgoux Aug 5, 2026
48c1da6
Merge remote-tracking branch 'origin/agent/stack-package-export-audit…
jgoux Aug 5, 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
7 changes: 7 additions & 0 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ can surface `Downloading` before normal runtime states. CLI-managed stacks use l
direct listeners and Realtime start with the stack, while HTTP services activate on first proxied
use. The package API itself keeps eager startup as its default.

Project files do not flow directly into `@supabase/stack`. The CLI's local-stack launch Adapter
loads one resolved project-config/environment snapshot, inspects the raw document where explicit
presence matters, and translates CLI exclusions, project paths, readiness policy, and pinned
runtime versions into the package-owned `StackConfig`. Its compile-checked parity ledger records
every project field as mapped, not applicable, or explicitly unsupported so later parity work
cannot silently add another command-local mapping.

Useful companion docs:

- [`../../packages/stack/docs/architecture.md`](../../packages/stack/docs/architecture.md)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { ProjectConfig } from "@supabase/config";
import { parseStorageSizeBytes, type ProjectConfig } from "@supabase/config";

import { diff } from "./config-sync.diff.ts";
import { encodeToml, type TomlField, type TomlValue } from "./config-sync.toml.ts";
import { bytesSize, intToUint, ramInBytes } from "../../../../shared/legacy-size-units.ts";
import { bytesSize, intToUint } from "../../../../shared/legacy-size-units.ts";

/**
* Push-subset of Go's `storage` struct (`pkg/config/storage.go`). `toml:"-"`
Expand Down Expand Up @@ -144,15 +144,15 @@ export function storageSubsetFromConfig(
name,
{
public: b.public,
file_size_limit: ramInBytes(b.file_size_limit),
file_size_limit: parseStorageSizeBytes(b.file_size_limit),
allowed_mime_types: b.allowed_mime_types,
objects_path: b.objects_path,
} satisfies BucketSubset,
]),
);
return {
enabled: s.enabled,
file_size_limit: ramInBytes(s.file_size_limit),
file_size_limit: parseStorageSizeBytes(s.file_size_limit),
image_transformation: presence.imageTransformation
? { enabled: s.image_transformation?.enabled ?? false }
: undefined,
Expand Down
6 changes: 3 additions & 3 deletions apps/cli/src/legacy/commands/start/lib/db-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ import {
legacyExecSqlFile,
} from "../../../shared/legacy-migration-apply.ts";
import type { LegacyMigrationSeedError } from "../../../shared/legacy-seed.ts";
import { ramInBytes } from "../../../shared/legacy-size-units.ts";
import { parseStorageSizeBytes } from "@supabase/config";
import {
LegacyMigrationVaultError,
legacyUpsertVaultSecrets,
Expand Down Expand Up @@ -344,7 +344,7 @@ function legacyStartStorageMigrateEnv(input: {
input.dbHost,
input.dbPassword,
),
FILE_SIZE_LIMIT: String(ramInBytes(input.fileSizeLimit)),
FILE_SIZE_LIMIT: String(parseStorageSizeBytes(input.fileSizeLimit)),
STORAGE_BACKEND: "file",
STORAGE_FILE_BACKEND_PATH: "/mnt",
TENANT_ID: "stub",
Expand Down Expand Up @@ -417,7 +417,7 @@ const legacyStartInitSchema15 = Effect.fnUntraced(function* (
}
if (input.config.storage.enabled) {
// `legacyStartStorageMigrateEnv` parses `storage.file_size_limit` via
// `ramInBytes`, which throws on a malformed value — a plain synchronous
// the canonical Storage size parser, which throws on a malformed value — a plain synchronous
// throw here would become an uncaught Effect defect (`Effect.tapError`'s
// rollback trigger below only fires on typed `Fail` causes, never `Die`
// ones), leaking Postgres's already-created container/network/volume.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { legacyServiceContainerName } from "../../../shared/legacy-docker-ids.ts
import {
legacyFormatGoDuration,
legacyParseGoDuration,
} from "../../../shared/legacy-go-duration.ts";
} from "../../../../shared/config/go-duration.ts";
import { LEGACY_DEFAULT_SIGNING_KEY } from "../../../shared/legacy-go-jwt.ts";
import type { LegacyResolvedAuthEmail } from "../../../shared/legacy-local-config-values.ts";
import type { LegacyStartContainerSpec } from "../lib/docker-create-args.ts";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,9 @@
* before passing `s3ProtocolEnabled`/`vectorBucketsEnabled` in.
*/

import type { ProjectConfig } from "@supabase/config";
import { parseStorageSizeBytes, type ProjectConfig } from "@supabase/config";

import { legacyServiceContainerName } from "../../../shared/legacy-docker-ids.ts";
import { ramInBytes } from "../../../shared/legacy-size-units.ts";
import type { LegacyStartContainerSpec } from "../lib/docker-create-args.ts";
import { legacyEnvOrDefault } from "../lib/legacy-env-or-default.ts";
import {
Expand Down Expand Up @@ -118,7 +117,7 @@ export interface LegacyStorageEnvInput {
readonly dbHost: string;
/** See `legacyStartInternalDbPassword` (`../lib/internal-db-connection.ts`). */
readonly dbPassword: string;
/** `config.storage.file_size_limit`, e.g. `"50MiB"` — converted to a byte count via `ramInBytes`. */
/** `config.storage.file_size_limit`, e.g. `"50MiB"` — converted to a byte count. */
readonly fileSizeLimit: ProjectConfig["storage"]["file_size_limit"];
/** `LegacyLocalConfigValues.storageS3Region`. */
readonly s3Region: string;
Expand Down Expand Up @@ -155,7 +154,7 @@ export function legacyBuildStorageEnv(input: LegacyStorageEnvInput): Record<stri
input.dbHost,
input.dbPassword,
),
FILE_SIZE_LIMIT: String(ramInBytes(input.fileSizeLimit)),
FILE_SIZE_LIMIT: String(parseStorageSizeBytes(input.fileSizeLimit)),
STORAGE_BACKEND: "file",
FILE_STORAGE_BACKEND_PATH: LEGACY_STORAGE_DOCKER_PATH,
TENANT_ID: "stub",
Expand Down
10 changes: 6 additions & 4 deletions apps/cli/src/legacy/commands/start/start.handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
inferFunctionsManifest,
parseStorageSizeBytes,
resolveProjectSubtree,
type ProjectConfig,
} from "@supabase/config";
Expand Down Expand Up @@ -46,9 +47,8 @@ import {
legacyDecryptSecret,
legacyIsEncryptedSecret,
} from "../../shared/legacy-vault-decrypt.ts";
import { ramInBytes } from "../../shared/legacy-size-units.ts";
import { legacyTempPaths } from "../../shared/legacy-temp-paths.ts";
import { legacyParseGoDuration } from "../../shared/legacy-go-duration.ts";
import { legacyParseGoDuration } from "../../../shared/config/go-duration.ts";
import {
LEGACY_CLI_PROJECT_LABEL,
legacyCliProjectFilterValue,
Expand Down Expand Up @@ -1415,13 +1415,15 @@ export const legacyStart = Effect.fn("legacy.start")(function* (flags: LegacySta
) ?? config.storage.file_size_limit;
// `@supabase/config`'s schema accepts `file_size_limit` as a plain string — it does not parse
// the size grammar itself, so a malformed value (e.g. "foobar") would otherwise only surface
// when `storage.service.ts`'s `ramInBytes` call builds the container env, well after
// when Storage's byte-size parser builds the container env, well after
// network/image/Postgres work, and never at all when Storage is excluded/disabled. Go's
// `sizeInBytes.UnmarshalText` (`pkg/config/config.go:39-49`) decodes this unconditionally in
// the same `Config.Load` pass as everything else (`config.go:749-756,775-784`), before `start`
// touches Docker or looks at `--exclude` — validate eagerly here to match, discarding the
// parsed byte count since every consumer re-parses `storageFileSizeLimit` itself.
yield* wrapConfigOverride("storage.file_size_limit", () => ramInBytes(storageFileSizeLimit));
yield* wrapConfigOverride("storage.file_size_limit", () =>
parseStorageSizeBytes(storageFileSizeLimit),
);
// Same gap for `storage.vector.enabled` — both the long-running Storage
// container AND `legacySeedBucketsRun`'s `effectiveLocalStorageConfig`
// splice further down must see the same already-overridden value (Go's
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/legacy/shared/legacy-db-config.toml-read.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseStorageSizeBytes } from "@supabase/config";
import { Effect, type FileSystem, Option, type Path } from "effect";
import * as SmolToml from "smol-toml";
import {
Expand All @@ -24,7 +25,6 @@ import {
import { LegacyDbConfigLoadError } from "./legacy-db-config.errors.ts";
import { parseDotEnv } from "./legacy-dotenv.ts";
import { legacyStrToArr } from "./legacy-local-config-values.ts";
import { ramInBytes } from "./legacy-size-units.ts";
import {
legacyCollectDotenvPrivateKeys,
legacyDecryptSecret,
Expand Down Expand Up @@ -1335,7 +1335,7 @@ const readDbTomlCore = Effect.fnUntraced(function* (
const limitString =
typeof rawLimit === "number" ? String(rawLimit) : legacyExpandEnv(rawLimit, lookup);
try {
ramInBytes(limitString);
parseStorageSizeBytes(limitString);
} catch {
return yield* Effect.fail(
new LegacyDbConfigLoadError({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -941,7 +941,7 @@ describe("legacyReadDbToml", () => {
it.effect("accepts a bare-number [storage.buckets.<name>].file_size_limit", () => {
// `@supabase/config`'s schema allows file_size_limit as either a quoted
// human-readable string or a bare byte count; the numeric form must normalize to
// a string before `ramInBytes` parses it rather than being rejected outright.
// a string before the canonical size parser sees it rather than being rejected outright.
const dir = withConfig("[storage.buckets.avatars]\nfile_size_limit = 5242880\n");
return read(dir).pipe(
Effect.tap(() => Effect.sync(() => rmSync(dir, { recursive: true, force: true }))),
Expand Down
96 changes: 4 additions & 92 deletions apps/cli/src/legacy/shared/legacy-size-units.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/**
* Ports of `github.com/docker/go-units` used by Go's `sizeInBytes`
* (`pkg/config/config.go`). `file_size_limit` config values are parsed with
* `RAMInBytes` and re-serialised in the diff with `BytesSize` (`sizeInBytes`
* implements `MarshalText`, so BurntSushi emits a quoted human-readable size,
* e.g. `"5MiB"`).
* Remaining formatting helpers from `github.com/docker/go-units` used by Go's
* `sizeInBytes` (`pkg/config/config.go`). Parsing is owned by
* `@supabase/config`; this module only re-serialises byte counts with
* `BytesSize` and preserves Go's signed-to-unsigned conversion behavior.
*
* Shared across the legacy shell: `config push` (storage/auth/api/db diffing)
* and `seed buckets` (which converts each `[storage.buckets.*].file_size_limit`
Expand All @@ -12,95 +11,8 @@
* @see github.com/docker/go-units@v0.5.0/size.go
*/

const BINARY_MAP: Readonly<Record<string, number>> = {
k: 1024,
m: 1024 ** 2,
g: 1024 ** 3,
t: 1024 ** 4,
p: 1024 ** 5,
};

const BINARY_ABBRS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] as const;

const DIGIT_OR_DOT_OR_SPACE = "0123456789. ";

/**
* Port of `units.RAMInBytes` — parses a human-readable RAM size (1024-based,
* case-insensitive, optional trailing `b`) into bytes. Throws on an unparseable
* string (Go returns an error that aborts config load).
*/
export function ramInBytes(sizeStr: string): number {
let sep = -1;
for (let i = 0; i < sizeStr.length; i++) {
if (DIGIT_OR_DOT_OR_SPACE.includes(sizeStr[i] as string)) sep = i;
}
if (sep === -1) {
throw new Error(`invalid size: '${sizeStr}'`);
}
let num: string;
let sfx: string;
if (sizeStr[sep] !== " ") {
num = sizeStr.slice(0, sep + 1);
sfx = sizeStr.slice(sep + 1);
} else {
num = sizeStr.slice(0, sep);
sfx = sizeStr.slice(sep + 1);
}
// Go's `RAMInBytes` (docker/go-units v0.5.0) hands the WHOLE numeric part to
// `strconv.ParseFloat`, which rejects a string that isn't a complete float.
// JS `Number.parseFloat` instead silently parses a valid prefix (`1.2.3` → 1.2,
// `1 2` → 1), so validate the numeric part against Go's float grammar first:
// optional sign, a leading OR trailing dot, optional exponent, and single
// underscores BETWEEN digits (Go 1.13+ literal rule — no leading/trailing/
// doubled `_`, none adjacent to `.`/sign). The digit group `\d(?:_?\d)*`
// enforces the underscore placement. This accepts Go-valid forms (`.5`, `1.`,
// `1e6`, `+5`, `1_000`) and rejects the prefix hazards (`1.2.3`, `1 2`,
// leading-space, `0x10`, `_1`, `1_`). A negative value is rejected post-parse
// below (matching Go's `size < 0` check); `1e309`→Infinity by the isFinite check.
if (
!/^[+-]?(?:\d(?:_?\d)*(?:\.(?:\d(?:_?\d)*)?)?|\.\d(?:_?\d)*)([eE][+-]?\d(?:_?\d)*)?$/.test(num)
) {
throw new Error(`invalid size: '${sizeStr}'`);
}
// Strip the (already-validated, between-digits) underscores before parsing:
// JS `Number.parseFloat("1_000")` stops at the underscore (→1), unlike Go.
const size = Number.parseFloat(num.replace(/_/g, ""));
// Reject NaN and ±Infinity: Go's `strconv.ParseFloat` returns a range error
// for an overflowing numeral like `1e309` (which JS parses to Infinity), so it
// must fail config load rather than flow through as `null` in the request body.
if (!Number.isFinite(size)) {
throw new Error(`invalid size: '${sizeStr}'`);
}
if (size < 0) {
throw new Error(`invalid size: '${sizeStr}'`);
}
if (sfx.length === 0) {
return Math.trunc(size);
}
if (sfx.length > 3) {
throw new Error(`invalid suffix: '${sfx}'`);
}
sfx = sfx.toLowerCase();
if (sfx[0] === "b") {
if (sfx.length > 1) {
throw new Error(`invalid suffix: '${sfx}'`);
}
return Math.trunc(size);
}
const mul = BINARY_MAP[sfx[0] as string];
if (mul === undefined) {
throw new Error(`invalid suffix: '${sfx}'`);
}
// The suffix may have a trailing "b" or "ib" (e.g. KiB or MB).
if (sfx.length === 2 && sfx[1] !== "b") {
throw new Error(`invalid suffix: '${sfx}'`);
}
if (sfx.length === 3 && sfx.slice(1) !== "ib") {
throw new Error(`invalid suffix: '${sfx}'`);
}
return Math.trunc(size * mul);
}

/**
* Port of Go's `fmt`-style `%.4g`: at most 4 significant digits, trailing zeros
* removed, no exponent for the magnitudes `BytesSize` produces (scaled to
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/legacy/shared/legacy-storage-bucket-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ramInBytes } from "./legacy-size-units.ts";
import { parseStorageSizeBytes } from "@supabase/config";
import type { LegacyUpsertBucketProps } from "./legacy-storage-gateway.ts";

/**
Expand All @@ -20,7 +20,7 @@ import type { LegacyUpsertBucketProps } from "./legacy-storage-gateway.ts";
* maps to a config-load error.
*/
export function legacyParseFileSizeLimit(sizeStr: string): number {
return ramInBytes(sizeStr);
return parseStorageSizeBytes(sizeStr);
}

function isRecord(value: unknown): value is Record<string, unknown> {
Expand Down
14 changes: 7 additions & 7 deletions apps/cli/src/next/commands/branches/switch/switch.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
ProjectLinkState,
ProjectNotLinkedError,
} from "../../../config/project-link-state.service.ts";
import { toStartStackConfig, withServiceVersions } from "../../../config/stack-config.ts";
import { resolveStoredStackLaunch } from "../../../config/stack-config.ts";
import { NonInteractiveError } from "../../../../shared/output/errors.ts";
import { Output } from "../../../../shared/output/output.service.ts";
import { RuntimeInfo } from "../../../../shared/runtime/runtime-info.service.ts";
Expand Down Expand Up @@ -143,13 +143,13 @@ export const switchBranch = Effect.fn("branches.switch")(function* (opts: {
// so the local config reflects the branch's migrations and seed state.
// `pull` does not exist yet.
const launchConfig = Option.match(maybeMetadata, {
onNone: () => toStartStackConfig([], "auto"),
onNone: () => resolveStoredStackLaunch({ exclude: [], mode: "auto", runtimeVersions: {} }),
onSome: (metadata) => {
const base =
metadata.launch !== undefined
? toStartStackConfig(metadata.launch.excludedServices, metadata.launch.mode)
: toStartStackConfig([], "auto");
return withServiceVersions(base, metadata.services);
return resolveStoredStackLaunch({
exclude: metadata.launch?.excludedServices ?? [],
mode: metadata.launch?.mode ?? "auto",
runtimeVersions: metadata.services,
});
},
});

Expand Down
Loading