Skip to content

feat(cli): port supabase stop and status commands to native TypeScript#5765

Open
Coly010 wants to merge 90 commits into
developfrom
cli/port-stop-status-commands
Open

feat(cli): port supabase stop and status commands to native TypeScript#5765
Coly010 wants to merge 90 commits into
developfrom
cli/port-stop-status-commands

Conversation

@Coly010

@Coly010 Coly010 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports supabase stop and supabase status from Go-proxy stubs to native TypeScript in the legacy CLI shell (CLI-1324).

  • Both commands now talk directly to Docker/Podman via subprocess, replicating Go's label-filtering and container-naming scheme byte-for-byte. Legacy start is still Go-proxied, so this intentionally does not route through @supabase/stack/effect's daemon-based orchestration model — that substrate manages a different set of containers than the ones Go's binary actually creates, and using it would silently no-op against a real running stack.
  • New shared infrastructure (legacy-docker-lifecycle, legacy-go-jwt, legacy-local-config-values, legacy-api-url) is reused by both commands, matching Go's local-dev defaults exactly — including a Go-byte-exact JWT signer, since @supabase/stack's own JWT generator uses a different issuer/claim order than what Go prints for local dev keys.
  • Adds *.live.test.ts as a documented test category (AGENTS.md) alongside unit/integration/e2e: black-box subprocess tests run by the cli-e2e-ci harness against a real platform. stop/status don't call the Management API, so their live tests spin up a real local Docker stack instead and verify against it directly (e.g. confirming Docker itself has no containers left after stop, not just trusting the CLI's exit code).

Notable review findings fixed along the way

  • Table/status output was colorizing based on stderr's TTY status while writing to stdout — piping stdout while stderr stayed a TTY (supabase status | less) would have corrupted output with ANSI escapes (the same bug class CLI-1546 fixed once before).
  • --override-name was leaking into pretty-mode output; Go's PrettyPrint rebuilds a fresh, un-overridden view and ignores it there.
  • --backup/--no-backup: Go's --backup flag is dead code (declared, never bound to a variable in cmd/stop.go) — the port now matches that exactly instead of an intended-but-never-true semantic.
  • Docker/Podman-both-missing errors now name the actual root cause instead of a generic "failed to ..." string.

Post-review hardening: scoping and consolidation

Two structural fixes on top of the port, addressing drift introduced while iterating on review feedback:

  • next/ no longer inherits Go-parity config semantics. The four viper-compat behaviors added during review (unconditional [remotes.*] project-id checks, the deprecated-provider WARN, the widened env() reference pattern, and comma-split coercion into array-typed fields) are now gated behind an opt-in goViperCompat flag on LoadProjectConfigOptions — default off restores the pre-review behavior for next/, packages/stack, and other non-parity consumers, following the same opt-in pattern as tomlOnly/skipEnvLocal. Only legacy-shell callers opt in. Regression tests pin the default-off behavior, including the next start config-load path that would otherwise have started hard-failing on a malformed [remotes.*].project_id.
  • Go's Config.Validate now has exactly one TS home. legacy/shared/legacy-config-validate.ts replaces the validation orchestration previously duplicated between legacy-db-config.toml-read.ts (db/migration family) and legacy-local-config-values.ts (status/stop); both callers build a normalized LegacyConfigValidationInput from their own pipelines and call the shared validator, and a cross-caller parity test feeds identical broken configs through both real pipelines asserting identical error strings. Consolidating surfaced and fixed three real divergences between the two copies: db.major_version = 0 now emits Go's Missing required field in config: db.major_version (the db loader previously emitted a non-Go message), and the captcha provider-enum and email content-vs-content_path checks are now present in both paths.

CLOSES CLI-1324

Coly010 added 2 commits July 2, 2026 11:11
Replaces the Go-proxy stubs for `stop`/`status` with native Effect
implementations that talk directly to Docker/Podman via subprocess,
replicating Go's label-filtering and container-naming scheme byte-for-byte.
Legacy `start` is still Go-proxied, so this intentionally does not go
through `@supabase/stack/effect`'s daemon-based orchestration model — that
substrate is incompatible with the real containers Go's binary creates.

Adds shared Docker/config infrastructure (`legacy-docker-lifecycle`,
`legacy-go-jwt`, `legacy-local-config-values`, `legacy-api-url`) used by
both commands, and fixes several correctness issues found during review:
stdout colorization keyed off the wrong stream's TTY status, `--override-name`
incorrectly leaking into pretty-mode output (Go ignores it there), and the
`--backup`/`--no-backup` formula not matching Go's actual (dead-flag)
behavior.

Fixes CLI-1324
…e.test.ts

Documents the *.live.test.ts convention in AGENTS.md (a 4th test category
alongside unit/integration/e2e): black-box CLI subprocess tests executed by
the cli-e2e-ci harness against a real supabox stack. Clarifies how
local-dev-stack commands like stop/status fit this pattern despite never
calling the Management API — they only need the real Docker daemon the
cli-e2e-ci runner also provides, so they reuse the existing describeLive
gate rather than a dedicated one.

Adds stop.live.test.ts and status.live.test.ts, each spinning up a real
local Docker stack (init -> start, excluding the heaviest services) in an
isolated temp directory and verifying the command under test against the
real containers, with best-effort cleanup on every path.
@Coly010 Coly010 self-assigned this Jul 2, 2026
@Coly010

Coly010 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

noBackup: Flag.boolean("no-backup").pipe(

P2 Badge Avoid colliding with backup's negated flag

Effect boolean flags register both the positive and negated token, so Flag.boolean("backup") above already claims --no-backup as the false form of the hidden backup flag. Defining a separate noBackup boolean on the same token can route supabase stop --no-backup to backup=false or otherwise collide during parsing, leaving flags.noBackup false and skipping the volume-prune path even though the user requested data deletion; model the hidden --backup=false flag without generating a --no-backup alias.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/commands/status/status.handler.ts Outdated
Comment thread apps/cli/src/legacy/commands/status/status.values.ts Outdated
Comment thread apps/cli/src/legacy/commands/stop/stop.handler.ts Outdated
Comment thread apps/cli/src/legacy/commands/status/status.handler.ts
@Coly010

Coly010 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for flagging this — I dug into it and don't think there's a collision here, though the concern was reasonable to raise given how the two flags look side by side.

Effect's flag registry (effect/unstable/cli/internal/parser.ts:232-252, current pinned version 4.0.0-beta.87) only indexes each flag's literal declared name (plus Flag.withAlias entries) at build time — it does not insert a synthetic no-<name> key for boolean flags. The --no-<name> negation is resolved lazily per-token in resolveFlag (parser.ts:348-380): it tries an exact-name match first (parser.ts:353-354) and only falls back to stripping the no- prefix and negating (parser.ts:361-369) when there's no direct match. Since noBackup is declared as Flag.boolean("no-backup"), its literal name "no-backup" is the direct match, so any --no-backup token resolves straight to noBackup — the hidden backup flag's negation path is never consulted, and there's no registry-level duplicate-name conflict either (backup and no-backup are distinct literal strings).

I confirmed this empirically by running the exact flag config through Command.runWith:

["--no-backup"]                 → { backup: true,  noBackup: true,  all: false }
[]                               → { backup: true,  noBackup: false, all: false }
["--backup=false"]              → { backup: false, noBackup: false, all: false }
["--no-backup", "--backup"]     → { backup: true,  noBackup: true,  all: false }

noBackup comes through correctly in every combination, and stop.handler.ts reads flags.noBackup (not a derived backup value) to drive the volume-prune path, so it isn't affected either way.

This also matches the Go CLI's own structure: cmd/stop.go:26-29 declares --backup as a hidden flag that's never actually read in RunE (Go's version is dead too) and --no-backup as a completely independent BoolVar. The two separate Flag.boolean declarations here are a deliberate 1:1 mirror of that, not an accidental duplication. Given that, I'll leave the flag declarations as-is, but appreciate you taking a close look at the flag-parsing semantics.

Coly010 added 3 commits July 2, 2026 12:13
…5765)

Three accepted findings from PR review, each verified against Go source
before implementing:

- Sanitize the config/env-derived project id before building the Docker
  label filter in both `stop` and `status`. Go's `Config.Validate`
  sanitizes `Config.ProjectId` once at config-load time
  (pkg/config/config.go:938-944), and every later reader — including the
  Docker label `start` writes (internal/utils/docker.go:375) — sees that
  same sanitized string. Without this, a dirty `project_id` (spaces,
  leading punctuation) would filter on a value `start` never labeled,
  silently matching nothing. The explicit `--project-id` bypass on `stop`
  stays raw, matching Go's stop.go:19-20.

- `status --override-name` with an unrecognized field key is now silently
  ignored instead of a hard error, matching Go's `go-env` Unmarshal, which
  never checks its input map for unmatched keys (verified against the
  vendored go-env@v0.1.2 source).

- `status` no longer hard-fails when `supabase/config.toml` is absent;
  Go's `flags.LoadConfig` treats a missing file as a no-op and proceeds
  with template defaults (pkg/config/config.go:655-656), so this now
  decodes an empty document through the shared config schema for its
  defaults instead of erroring. (The broader gap — Go's automatic
  `SUPABASE_<FIELD>` env-var binding via viper, which `@supabase/config`
  doesn't have an equivalent for — is a larger, cross-cutting
  `@supabase/config` feature affecting every ported command, called out
  as a follow-up rather than folded into this fix.)

Two other findings were investigated and rejected with cited evidence
(directly on the PR): a claimed `--backup`/`--no-backup` flag collision
(empirically disproven against Effect's parser), and image-name-based
`--exclude` matching (the underlying config.toml schema has no field to
check, on either CLI).
…0O86N3Lyo)

Go's status.toValues() gates each service on `--exclude` matching either
the container id or the Docker image's short name (ShortContainerImageName).
The TS port only checked container ids. Port the short-name extraction and
check it against the same default images the embedded Dockerfile manifest
already provides, since the relevant Go config fields (KongImage, Image,
etc.) all carry `toml:"-"` and are never user-overridable.
…us-commands

# Conflicts:
#	apps/cli/docs/go-cli-porting-status.md
@Coly010

Coly010 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b42381e79

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/commands/stop/stop.handler.ts Outdated
Comment thread apps/cli/src/legacy/commands/status/status.handler.ts
Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts Outdated
Comment thread apps/cli/src/legacy/commands/status/status.handler.ts
Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts Outdated
Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b42381e79

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/commands/stop/stop.handler.ts Outdated
Comment thread apps/cli/src/legacy/commands/status/status.handler.ts
Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts Outdated
Comment thread apps/cli/src/legacy/commands/status/status.handler.ts
Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts Outdated
Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts Outdated
Coly010 added 8 commits July 2, 2026 13:25
…OErm0O86N4wLX)

Go's stop.Run checks len(projectId) > 0 (internal/stop/stop.go:18), not just
whether --project-id was set, so an explicit but empty value falls through
to config.toml resolution. The TS port only checked Option.isSome, so
`--project-id ""` resolved to the bare all-projects label filter instead.
…6N4wLa, PRRT_kwDOErm0O86N4wLu)

Go registers both --override-name and --exclude as pflag StringSliceVar
(cmd/status.go:36-37), which CSV-splits each occurrence and accumulates
across repeats. The TS flags only handled repetition, so a single
comma-separated value like `--exclude kong,auth` produced one malformed
entry instead of two. Reuses the shared legacyParseStringSliceFlag already
applied to sso/postgres-config for the same StringSlice parity gap.
…6N4wL1)

Go's Config.Validate fails config-load when auth.jwt_secret is set but
shorter than 16 characters (pkg/config/apikeys.go:45-47), before any command
can render output. The TS resolver accepted any non-empty value and signed
ANON_KEY/SERVICE_ROLE_KEY with it, letting `status -o env/json` succeed and
print keys for a config the Go CLI and local stack both reject.
… PRRT_kwDOErm0O86N4wLx)

Go's config loader binds Viper with SetEnvPrefix("SUPABASE") + AutomaticEnv()
(pkg/config/config.go:529-535), so SUPABASE_AUTH_JWT_SECRET/PUBLISHABLE_KEY/
SECRET_KEY/ANON_KEY/SERVICE_ROLE_KEY override the corresponding config.toml
value at higher precedence. legacyResolveLocalConfigValues only read the
decoded config object, so a local stack started with those env overrides
had `status` print keys that didn't match the running Auth service. Scoped
to exactly the 5 auth fields this module reads, not a general
@supabase/config port of Viper's AutomaticEnv.
…ed (review: PRRT_kwDOErm0O86N4wLk)

Go's generateJWT signs anon/service_role with the first key in
auth.signing_keys_path (RS256/ES256) instead of HMAC when that file
resolves to a non-empty JWK array (pkg/config/apikeys.go:76-113). The TS
resolver only ever HMAC-signed with jwt_secret. Ports GenerateAsymmetricJWT
via legacyGenerateAsymmetricGoJwt (RFC 7517 JWK -> Node crypto private key,
ieee-p1363 signature encoding for ES256's raw r||s format) and wires it
into legacyResolveLocalConfigValues, which now needs workdir to resolve a
relative signing_keys_path against <workdir>/supabase.
Go's jwkToRSAPrivateKey/jwkToECDSAPrivateKey reject a JWK whose kty doesn't
match its claimed alg, and an EC key whose curve isn't P-256
(pkg/config/auth.go). legacyGenerateAsymmetricGoJwt only checked alg, so an
EC key forged with alg: RS256 (or a non-P-256 curve claimed as ES256)
signed "successfully" and produced a spec-invalid token that silently
fails verification instead of raising an error. Found independently by the
security and DX reviewers in review-changes.
…tusValues twice

status.handler.ts called legacyStatusValues twice per invocation (real
values, then pretty-mode values with an empty override map) — only the
first call was wrapped in Effect.try. With signing_keys_path support this
doubled file I/O + JWT signing on every text-mode run, and left the second
call able to throw an uncaught exception if it ever diverged from the
first. Splits legacyStatusValues into legacyResolveStatusState (the
throwing half: local config resolution + gating) and
legacyStatusValuesFromState (pure name remapping), so the handler resolves
state once, guards it once, and reuses it for both value maps.
…ration coverage

Go's Config.Validate fails a bad auth.signing_keys_path with "failed to
read signing keys: %w" (open failure) or "failed to decode signing keys:
%w" (parse failure) (pkg/config/config.go:1059-1062). The TS port let
readFileSync/JSON.parse's raw Node error text through unwrapped instead.
Also adds status.integration.test.ts coverage for the SUPABASE_AUTH_* env
override and asymmetric-signing-key behaviors, which previously only had
unit-level coverage on the pure resolver.
@Coly010 Coly010 marked this pull request as ready for review July 2, 2026 14:06
@Coly010 Coly010 requested a review from a team as a code owner July 2, 2026 14:06
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Supabase CLI preview

npx --yes https://pkg.pr.new/supabase/cli/supabase@3c6d766f35cc6b8d852c94c556a54d243b7b99eb

Preview package for commit 3c6d766.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60877ba497

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts Outdated
Comment thread apps/cli/src/legacy/commands/stop/stop.handler.ts Outdated
Coly010 added 5 commits July 2, 2026 15:49
…spection (ci: e2e shard 1/3)

Go's assertContainerHealthy never special-cases a missing container — it
wraps whatever ContainerInspect returns, so the real daemon error text
("No such container: ...") flows through. The TS port collapsed that case
into a hardcoded "no such container" string via an "absent" sentinel,
discarding the real text.

Separately, legacyInspectContainerState's Effect.all([exitCode, stdout,
stderr]) ran sequentially by default, awaiting exitCode (Node's "exit"
event) before ever subscribing to the stdout/stderr streams. Node's "exit"
can fire before a fast process's stdio pipes are drained, so the real
Docker CLI's stderr was silently lost in the real subprocess environment
even after removing the hardcoded string — reproduced against a real
docker CLI subprocess, confirmed via runParity's stderr comparison in
apps/cli-e2e. Same fix applied to the two other call sites in this file
that share the pattern (legacyListContainersByLabel, legacyListVolumesByLabel).
…e2e shard 1/3)

Go prints this line via an unconditional, immediate fmt.Fprintln before any
Docker call runs (docker.go:97), routed straight to stdout in non-interactive
mode (tea.go's fakeProgram). The TS port gated it behind the shared
output.task spinner's 200ms debounce, so the line was silently dropped
whenever the underlying Docker calls resolved faster than that threshold —
exactly what happens against the mocked/replayed Docker CLI in the e2e
harness. Printing it directly via output.raw removes the race entirely.
…ity request logs (ci: e2e shard 1/3)

stop/status parity comparisons were failing on request-log mismatches that
reflect nothing about CLI behavior: the real docker CLI issues a fresh
HEAD /_ping handshake before every subprocess invocation (Go's SDK pings
once per command via a persistent client), and negotiates its own API
version segment into the URL path (/v1.51/ vs /v1.53/) based on the
installed docker CLI/daemon, not anything the command controls. Strip both
before comparing so parity reflects the actual Docker operations performed
rather than client plumbing, mirroring the equivalent normalization already
used for fixture matching in apps/cli-e2e/src/server/placeholder.ts.
…s (review: PRRT_kwDOErm0O86N7ctR)

Go's Viper binds SetEnvPrefix("SUPABASE") + AutomaticEnv() over every
config field via UnmarshalExact's struct walk (pkg/config/config.go:531-535,
698-705), including the plain-string Auth.SigningKeysPath field
(pkg/config/auth.go:164) — not just the 5 auth fields this module already
wrapped with envOverride. Load() resolves that override before Validate()
opens and parses the JWK file (config.go:735-745, 1059-1062), so an
env-only SUPABASE_AUTH_SIGNING_KEYS_PATH was silently ignored and status
fell back to HMAC-signed keys the running Auth service would reject.
… status (review: PRRT_kwDOErm0O86N7ctY)

Go's Config.Load runs loadNestedEnv (supabase/.env and .env.local via
godotenv.Load, which never overrides an already-set var) before
loadFromFile wires up Viper's AutomaticEnv (pkg/config/config.go:735-745,
528-535) — so an env-file-only SUPABASE_PROJECT_ID overrides config.toml's
project_id too, not just an ambient shell export. Both handlers only read
process.env directly, missing that middle step: an env-named stack could be
left running by stop, or status could target the wrong container.

@supabase/config's loadProjectEnvironment already implements the same
"ambient wins over .env/.env.local" layering (used today only for env()
interpolation inside config.toml), so both handlers now resolve
SUPABASE_PROJECT_ID through it instead of process.env directly, and pass
the same resolved environment into loadProjectConfig so the .env files
aren't parsed twice. Falls back to process.env directly when no
supabase/config.toml exists anywhere (loadProjectEnvironment resolves to
null in that case), preserving the existing ambient-only behavior for that
scenario.

The identical gap in LegacyCliConfig.projectId (used by several other
already-ported commands) is out of scope here and left for a follow-up.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb101635cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts Outdated
Comment thread apps/cli/src/legacy/commands/stop/stop.handler.ts Outdated
Comment thread apps/cli/src/legacy/commands/stop/stop.handler.ts
Comment thread apps/cli/src/legacy/commands/stop/stop.handler.ts Outdated
Coly010 added 2 commits July 6, 2026 18:48
…(review: #5765)

Go's Config.Validate parses studio.api_url with net/url.Parse right
after the studio.port check, still inside if c.Studio.Enabled
(pkg/config/config.go:1074-1078), rejecting malformed hosts such as an
unterminated IPv6 literal (http://[::1). legacyResolveLocalConfigValues
never checked studio.api_url at all, so native status/default stop
could accept a config Go rejects.

Extends legacy-storage-url.ts's legacyGoUrlParse (previously
Scheme/Path only, since storage ss:// URLs never have a host) with a
faithful port of net/url.parseHost's bracket/port validation, verified
byte-for-byte against the real Go stdlib.
…r (review: #5765)

Go's Config.Validate rejects [auth.passkey] enabled = true configured
without a complete [auth.webauthn] (rp_id/rp_origins), right after the
signing-keys read and before Auth.Hook.validate()
(pkg/config/config.go:1117-1129). legacyResolveLocalConfigValues never
read this at all, so native status/default stop could continue for a
config Go refuses.

@supabase/config's auth schema has no passkey/webauthn fields, so this
threads LoadedProjectConfig.document (the raw, pre-schema-default TOML)
through legacyResolveLocalConfigValues -> legacyResolveStatusLocalState
-> status.handler.ts/stop.handler.ts, the same document-based approach
already used for the equivalent check on the db/migration config-load
path (legacy-db-config.toml-read.ts's legacyValidateAuthConfig, A6).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b8d4e9f73

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

projectEnvValues: Readonly<Record<string, string>> | undefined,
): number {
const value = envOverride(name, undefined, projectEnvValues);
if (value === undefined) return configuredPort;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate configured ports before returning them

The current helper still returns the decoded configuredPort unchecked whenever there is no direct SUPABASE_*_PORT override, and the shared config schema declares ports as plain numbers. When the value comes from config.toml or an env() substitution, such as db.port = 70000 or -1, Go rejects it while decoding the uint16 field, but this path can continue and emit URLs with an invalid port; apply the same integer/range validation to the configured value too.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same claim already raised and addressed on this PR (see the resolved discussion on envOverridePort, same file/function): Go's db.port/api.port/etc. decode as uint16 via viper.UnmarshalExact → mapstructure's decodeUint. For a literal TOML integer like 70000, that decode does not hard-fail — reflect.Value.SetUint's truncating conversion silently wraps it to 4464 (70000 mod 65536), and Go proceeds with the wrapped value (verified by building and running that decoder against the identical input). The hard-fail path only exists for the string-sourced SUPABASE_*_PORT env-var override, which goes through mapstructure's WeaklyTypedInput string branch (strconv.ParseUint(str, 0, 16)) — exactly the case envOverridePort's existing MAX_PORT check already covers.

Adding range validation to the plain configuredPort value (as proposed here) would make native status/stop reject literal TOML ports that the real Go CLI silently accepts (with wraparound) — a step away from parity, not toward it. Not making this change, for the same reason as before.

Comment thread packages/config/src/io.ts Outdated
Comment thread apps/cli/src/legacy/commands/status/status.handler.ts
new LegacyStatusConfigLoadError({ message: `failed to read config: ${String(cause)}` }),
});

const loaded = yield* loadProjectConfig(cliConfig.workdir, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate auth subvalidators on auth.enabled

When [auth] enabled = false, Go skips Auth.Sms.validate() and Auth.External.validate() entirely, so stale nested blocks such as [auth.sms.twilio] enabled = true without credentials or [auth.external.github] enabled = true without a secret do not block status/default stop. This load goes through @supabase/config, whose nested requiredWhenEnabled checks run without the parent auth.enabled gate, so native status can reject a disabled auth configuration that the Go CLI accepts; defer those auth sub-section checks to the post-load Go-parity validator.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed as a real gap, not a no-op: Go's Config.Validate nests Auth.Sms.validate()/Auth.External.validate() entirely inside if c.Auth.Enabled (apps/cli-go/pkg/config/config.go:1087,1145,1148), so a disabled auth section with a stale enabled-but-uncredentialed [auth.sms.twilio]/[auth.external.github] block validates fine in Go. In @supabase/config, requiredWhenEnabled (packages/config/src/auth/sms.ts, packages/config/src/auth/providers.ts) only checks each nested provider's own enabled flag, never the parent auth.enabled, so the schema rejects that same config unconditionally during decode — this repo's own legacy-local-config-values.ts:1229-1232 doc comment already flags Sms/External as the two Go-gated sub-checks not yet ported into the (correctly authEnabled-gated) post-load validator.

Holding off on making this change in this run, though: the only place that actually stops the unconditional check from firing is @supabase/config's schema itself (sms.ts/providers.ts), since a nested struct's own .check() fails during decode before any caller-side post-load validator gets a chance to run. That schema is shared by every native command that calls loadProjectConfigconfig push, gen types, seed buckets, functions new, functions serve/deploy, not just status/stop — so removing the check there and re-adding it only in status/stop's own legacy-local-config-values.ts would silently drop this validation for every other command, including the auth.enabled=true case those commands correctly reject today. The parity-correct fix is to relocate the check to a cross-field .check() on the top-level auth struct (gated on auth.enabled, still enforced for every caller), not a status/stop-local change — that's real surgery on shared, widely-consumed schema code well beyond this port's stated scope, and I'd rather not make it unilaterally without the coverage/verification pass across every other loadProjectConfig consumer it deserves.

Flagging for @Coly010's call: happy to take this on as a follow-up against @supabase/config if you'd like it tracked separately, or to attempt it inline here if you'd rather have it in this PR.

new LegacyStatusConfigLoadError({ message: `failed to read config: ${String(cause)}` }),
),
);
const config = loaded?.config ?? Schema.decodeUnknownSync(ProjectConfigSchema)({});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject TS-only function env blocks

The shared config schema accepts [functions.<slug>.env] records, but the Go function config struct has no env field and v.UnmarshalExact rejects that TOML before status or default stop reaches Docker. By trusting this decoded ProjectConfig without an exact Go-shape check, native status can proceed for a config the Go CLI refuses; add a raw-document validation for function keys that are not in the Go struct before continuing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for digging into this — you're right about Go's behavior: v.UnmarshalExact (apps/cli-go/pkg/config/config.go:749) does hard-fail on an unknown key like [functions.<slug>.env] (Go's function struct has no env field, config.go:290-296). I reproduced this directly against the pinned viper/mapstructure versions and confirmed Go rejects it with 'functions[foo]' has invalid keys: env.

That said, env isn't an accidental leftover in @supabase/config's schema — it's the intentional per-function allowlist for the newer environments/config.json system (docs/environments-management.md), and the permissive decode (no Go-struct-exactness check on unknown keys) is a property of @supabase/config shared by every legacy command that calls loadProjectConfigstop, config push, secrets set, gen types, functions new, etc. all go through the identical tomlOnly fallback path and none of them do Go-shape validation either. Fixing this only in status.handler.ts would leave the identical gap open in all of those and produce inconsistent strictness rather than real parity.

The right fix is a TOML-vs-JSON-aware "reject keys Go's struct doesn't have" guard centralized in packages/config, applied uniformly wherever a tomlOnly load happens — not a per-command patch, and given it needs to distinguish the TOML-only Go-compat path from the JSON/config.json path (which legitimately allows env), it's more involved than this thread's scope. Not making a status/stop-local change here; happy to see this tracked as a follow-up against @supabase/config instead.

Comment thread apps/cli/src/legacy/commands/status/status.handler.ts
Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts
Coly010 added 2 commits July 6, 2026 19:28
…NALYTICS_BACKEND overrides (review: #5765)

Two Go-parity gaps in the status/stop config resolver:

- Go defaults `auth.email.smtp.enabled = true` when `[auth.email.smtp]` is
  present but omits `enabled` (pkg/config/config.go:743-748), a presence-based
  default set on the raw viper map before the struct decodes. The shared
  schema always decodes a missing `enabled` key to `false`, erasing that
  presence signal, so this reads the raw TOML document instead (same
  approach already used for passkey/webauthn and experimental.webhooks).
  Mirrors the equivalent, already-correct check on the db/migration
  config-load path (legacy-db-config.toml-read.ts).

- `SUPABASE_ANALYTICS_BACKEND` env overrides weren't validated against Go's
  `LogflareBackend` enum (config.go:60-65), so a bad override (e.g. "mysql")
  would continue where Go's `UnmarshalText` hard-fails config load. Validates
  the override-or-configured value with a single check, matching how viper
  merges both sources into one string before UnmarshalExact runs
  UnmarshalText exactly once.
…chema decode (review: #5765)

Go's `(e external) validate()` (pkg/config/config.go:1418-1423) unconditionally
deletes the deprecated `linkedin`/`slack` provider keys before the
required-field loop runs, warning on stderr only when the deleted provider
was enabled — a bare `[auth.external.slack] enabled = true` with no
client_id/secret loads fine in Go. The shared schema still modeled `slack`
as a fully-required provider (and had no `slack_oidc` entry at all, unlike
`linkedin_oidc`), so `loadProjectConfig` rejected a config Go accepts. This
affected every command that loads project config (status, stop, push, pull),
not just status/stop.

Adds `normalizeDeprecatedExternalProviders` in io.ts, mirroring the existing
`normalizeDeprecatedSMTPSections`/`[inbucket]` precedent, and renames the
`slack` provider to `slack_oidc` in the schema (config-sync/auth.sync.ts
already had unreachable `slack_oidc` cases, confirming this was the intended
id). Runs on the post-remote-merge document, not pre-merge like the SMTP
normalizer, since Go's external.validate() runs once on the final effective
config rather than eagerly over every [remotes.*] entry.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46916aac3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts Outdated
Comment thread packages/config/src/io.ts Outdated
);
}

const config = yield* parseProjectConfig(normalizedForDecode, format, filePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid validating inactive remote subtrees

When no projectRef is selected, remotes is still present in normalizedForDecode, so ProjectConfigSchema validates every nested remote block. That rejects configs Go accepts, e.g. an unselected [remotes.prod.auth.external.github] enabled = true stub without a secret, because Go's Config.Validate only checks remotes.*.project_id for remote blocks unless one is merged into the effective base config; status/default stop can therefore fail before Docker on a remote subtree Go ignores.

Useful? React with 👍 / 👎.

Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts Outdated
Comment on lines +1212 to +1215
// `Auth.Email.validate()`, then (skipping the unported Sms/External steps)
// `Auth.ThirdParty.validate()`, all right after the signing-keys read and
// still inside `if c.Auth.Enabled` (`pkg/config/config.go:1117-1153`).
// Sms/External (`config.go:1145-1150`) aren't ported.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate unknown auth.external providers

When auth is enabled and config.toml declares an arbitrary OAuth provider such as [auth.external.custom] enabled = true without client_id/secret, Go decodes auth.external as a map and validates every enabled provider, so config loading fails before Docker access. The fixed TS schema drops provider keys it does not model and this block explicitly skips External, letting native status/default stop accept configs the Go CLI rejects; validate enabled raw auth.external.* entries under the same authEnabled gate.

Useful? React with 👍 / 👎.

Coly010 added 4 commits July 6, 2026 19:56
…rify (review: #PRRT_kwDOErm0O86OqN9V)

Go's jwkToRSAPrivateKey (pkg/config/apikeys.go:132-168) builds the RSA
private key from n/e/d/p/q alone and its stdlib derives dp/dq/qi lazily
when absent, so a signing-keys JWK missing those CRT exponents still
signs successfully in Go. Node's createPrivateKey({ format: "jwk" })
has no such fallback and hard-rejects the same key, so native status
failed to generate local anon/service keys for a file the Go CLI
accepts. Compute the missing dp/dq/qi via BigInt before handing the
JWK to Node, matching Go's derivation.
…ew: #PRRT_kwDOErm0O86OqN9Z)

Go's LoadEnvHook is the first mapstructure decode hook composed into
UnmarshalExact (config.go:749-753,769-772), so it resolves a nested
env(VAR) reference on any string mapstructure decodes into the struct,
including a SUPABASE_* AutomaticEnv override string itself, not just a
config.toml literal. envOverride returned that raw override string
verbatim, so e.g. SUPABASE_API_ENABLED=env(API_ENABLED) was rejected as
a malformed bool instead of resolving to the referenced value. Resolve
one level of env(VAR) indirection with the same projectEnvValues/
process.env precedence and non-empty gate as the outer lookup, reusing
packages/config's ENV_CAPTURE_REGEX (now re-exported) rather than
duplicating the pattern.
…iew: #PRRT_kwDOErm0O86OrVR-, #PRRT_kwDOErm0O86OrzAH)

Go's loadFromFile duplicate-check/selection loop over [remotes.*]
reads viper's raw string values (config.go:596-610) and only resolves
env(...) afterward, inside the struct decode's LoadEnvHook
(config.go:611,749-753, decode_hooks.go:13-26). loadProjectConfigFile
instead ran the match/merge against the already-interpolated document,
so a [remotes.x] project_id = "env(REF)" would match a caller-supplied,
already-resolved REF even though Go compares the literal env(REF)
string and never selects that remote. Run the duplicate-check and
match/merge against the raw, pre-interpolation document, then
interpolate the merged result for the final decode. The project_id
FORMAT check (Go's post-decode Config.Validate) still uses the
interpolated remotes, since that check genuinely runs after env()
resolution in Go.
… #PRRT_kwDOErm0O86OrVSC)

Go's Docker.DaemonHost() (internal/utils/docker.go:41-54) comes from a
client built via cli.Initialize(), whose endpoint resolution walks
DOCKER_HOST -> DOCKER_CONTEXT -> the config file's currentContext ->
the context store (docker/cli's getDockerEndPoint/resolveContextName)
- not just DOCKER_HOST. legacyGetHostname only checked
SUPABASE_SERVICES_HOSTNAME/DOCKER_HOST and fell back to 127.0.0.1, so
with an active non-default Docker context and no DOCKER_HOST set,
native status could correctly inspect a remote daemon (the docker/
podman binary its ps/inspect calls shell out to already honors the
context) while printing unusable localhost API/DB/Studio URLs for it.
Resolve the active context's tcp:// endpoint from
$DOCKER_CONFIG/config.json and the context store's meta.json before
falling back to the loopback default, mirroring Go's precedence.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b15c7dd935

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +61 to +62
Command.withHandler((flags) =>
legacyStop(flags).pipe(withLegacyCommandInstrumentation({ flags }), withJsonErrorHandling),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unsupported output formats before stopping

When supabase stop -o csv or -o table is used, the TS global flag parser accepts those db-query-only values and this command proceeds into legacyStop, so containers can be stopped even though the Go CLI would reject the flag before stop.RunE runs. I checked the Go wiring: cmd/root.go:330 registers the root --output enum, and internal/utils/output.go:30-37 allows only env|pretty|json|toml|yaml; table|csv are not valid for stop. Please reject unsupported LegacyOutputFlag values for this native command before invoking the handler.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for flagging this, but I dug into it and it's already handled — just not visibly in this file. stop.command.ts:62 wraps the handler with withLegacyCommandInstrumentation({ flags }), and that wrapper defaults outputFormats to LEGACY_RESOURCE_OUTPUT_FORMATS (env|pretty|json|toml|yaml, see legacy-go-output-flag.ts) whenever a command doesn't override it. It runs legacyValidateOutputFormat before the handler executes, the same mechanism status and other resource commands use to compensate for Effect CLI's single shared --output enum, since we can't register a per-command EnumFlag the way Go's cmd/root.go:330/internal/utils/output.go:30-37 does.

I confirmed on this branch that stop -o csv and stop -o table both fail immediately with invalid argument "csv" for "-o, --output" flag: must be one of [ env | pretty | json | toml | yaml ] and exit 1 — no Docker calls happen, matching Go's pre-RunE pflag rejection. So no code change is needed here.

I did find that stop's SIDE_EFFECTS.md had a stale/incorrect note claiming Go has no -o/--output flag at all and that TS accepting an unknown value was a "harmless divergence" — that's backwards (Go's -o is a root PersistentFlags() enum every subcommand inherits, and TS already rejects the same values). Fixed that doc in 3c6d766.

Comment on lines +326 to +329
if (goFmt === "pretty") {
yield* renderPretty();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject db-only output formats before status

When supabase status -o csv or -o table is used, the TS root parser accepts these db-query-only values, but this branch does not reject them and falls through to the normal pretty/status path after config loading and Docker inspection. In Go, the root --output enum registered at apps/cli-go/cmd/root.go:330 only allows env|pretty|json|toml|yaml (apps/cli-go/internal/utils/output.go:30-37), so those invocations fail during flag parsing before status.Run touches Docker; add the same command-level rejection here for unsupported LegacyOutputFlag values.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for flagging this, but I don't think there's a gap here — status -o csv/-o table are already rejected before status.handler.ts runs any Docker/config logic. The rejection lives one layer up, in status.command.ts, which passes outputFormats: LEGACY_RESOURCE_OUTPUT_FORMATS (env|pretty|json|toml|yaml, matching Go's root --output enum at cmd/root.go:330/internal/utils/output.go:30-37) to withLegacyCommandInstrumentation. That wrapper validates the flag and fails before invoking the handler effect at all — the same way Go's cobra rejects an out-of-enum -o at flag-parse time, before RunE.

I confirmed this by running status -o csv locally — it prints invalid argument "csv" for "-o, --output" flag: must be one of [ env | pretty | json | toml | yaml ] and exits without touching Docker, byte-identical to what Go would say. table/csv are only valid for db query's own separate -o enum (cmd/db.go), which shadows the root persistent flag in cobra — status was never meant to accept them, and it doesn't. No code change needed; adding a second inline check in the handler would just duplicate validation this codebase intentionally centralizes in one place.

The companion stop thread raised the identical claim and turned out to be the same false positive (stop.command.ts inherits the same default gate) — replied there separately, and also fixed a stale doc note in stop's SIDE_EFFECTS.md that this investigation surfaced.

Comment on lines +173 to +174
const loaded = yield* loadProjectConfig(cliConfig.workdir, {
projectEnv: projectEnv !== null ? { ...projectEnv, values: projectEnvValues } : undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mirror Go's SMS provider switch

When auth is enabled and multiple SMS providers are present, Go's sms.validate() uses a switch over providers, so if [auth.sms.twilio] is enabled and complete, a later incomplete [auth.sms.messagebird] enabled = true block is not inspected and status/default stop continue. This load path decodes through @supabase/config, whose per-provider requiredWhenEnabled checks validate every enabled SMS provider, so native status and default stop can reject a stale secondary SMS block that the Go CLI accepts before reaching Docker.

Useful? React with 👍 / 👎.

Coly010 added 2 commits July 6, 2026 20:24
…o (review: #PRRT_kwDOErm0O86OrzAK)

Go's loadDefaultEnv (apps/cli-go/pkg/config/config.go:1243-1250) omits
.env.local from its candidate dotenv list whenever SUPABASE_ENV=test, so
a malformed or intentionally non-test supabase/.env.local is invisible
to Go in that mode. status/stop's preliminary loadProjectEnvironment
call had no such gate and its parse failure propagates as a real
command failure, unlike the later legacyResolveProjectEnvironmentValues
pass which already re-derives the file list per SUPABASE_ENV.

Add an opt-in skipEnvLocal option to loadProjectEnvironment (default
off, so next/ and secrets set are unaffected) and pass it from both
status.handler.ts and stop.handler.ts when SUPABASE_ENV=test.
…ields (review: #PRRT_kwDOErm0O86OrzAR)

Go's newDecodeHook (apps/cli-go/pkg/config/config.go:775-784) wires
mapstructure.StringToSliceHookFunc(",") unconditionally into config
decode, so a plain string value for a []string field (e.g.
auth.additional_redirect_urls = "http://a,http://b") decodes fine in
Go, whether the string is a literal TOML value or an env()-substituted
one. @supabase/config's schema had no equivalent and rejected any
string value for an array-typed field outright.

Extend the existing pre-decode env() coercion walker
(interpolateEnvReferencesAgainstSchema) with an "array" ExpectedType
for homogeneous string arrays, and apply it to any string leaf at that
path (not just env()-substituted ones, since Go's hook fires
regardless of value source) by splitting on "," -- matching Go's
StringToSliceHookFunc exactly, including its empty-string-to-empty-
slice case.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09e218a2e4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/commands/status/status.handler.ts
Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts Outdated
Comment thread apps/cli/src/legacy/commands/status/status.handler.ts
Comment thread packages/config/src/lib/env.ts
Comment thread apps/cli/src/legacy/shared/legacy-local-config-values.ts Outdated
Comment thread packages/config/src/io.ts Outdated
Coly010 added 9 commits July 7, 2026 12:28
PR #5765 wired four Go/viper-parity behaviors unconditionally into the
shared packages/config load pipeline, leaking Go-only semantics into
next/, packages/stack, and the functions manifest. Gate all four behind
a new opt-in LoadProjectConfigOptions.goViperCompat (default false),
mirroring the existing tomlOnly/skipEnvLocal precedents:

1. Unconditional [remotes.*] duplicate-project_id/format checks, even
   without a projectRef (Go: config.go:594-602,996-1001).
2. The deprecated auth.external.{linkedin,slack} WARN on stderr
   (config.go:1418-1423) — the strip itself stays unconditional.
3. The widened env(...) reference pattern (case-agnostic) vs. the
   strict SCREAMING_SNAKE_CASE pre-PR pattern.
4. Comma-split coercion of a plain string into a []string field, not
   just an env()-substituted one (mapstructure.StringToSliceHookFunc).

Default-off restores pre-#5765 behavior for every consumer that
doesn't opt in. Only the Go-parity legacy shell sets it to true.
…ntics

The legacy shell is a strict 1:1 Go CLI port, so every loadProjectConfig/
resolveProjectSubtree/resolveProjectValue call site it owns now sets
goViperCompat: true, restoring the four Go/viper decode+validation
behaviors gated behind that flag in packages/config.

Threads goViperCompat through the two shared functions modules
(shared/functions/deploy.ts, shared/functions/serve.ts) as a required
dependency field so each shell's handler makes an explicit choice:
legacy sets it true, next/ sets it false. Adds regression coverage
pinning next/'s default-off behavior (functions dev config, next start's
config-load path) and legacy's Go-parity-on behavior (status/stop
duplicate and malformed [remotes.*] handling, comma-split array
decoding, deprecated-provider stderr warning).
…g.Validate parity

Relocates the 5 shared Go-parity regex patterns (bucket name, function slug,
hook secret, clerk domain, project ref) and legacyParseGoBool out of
legacy-db-config.toml-read.ts into a new legacy-config-validate.ts, verbatim
and with no behavior change. legacy-local-config-values.ts's existing import
of the same symbols is repointed at the new module.

This is a purely mechanical move: it establishes the single home the
Config.Validate validator itself will land in during a follow-up commit, per
apps/cli/AGENTS.md's "Hoist Before You Duplicate" policy.
…ig-validate

Implements legacyValidateResolvedConfig in legacy-config-validate.ts, porting
every Config.Validate branch L (legacy-local-config-values.ts) previously
duplicated inline (storage buckets, studio, local_smtp, auth site_url/captcha/
passkey/hooks/mfa/smtp/third_party, function slugs, deno_version, analytics,
experimental) into the single shared home, plus the 3 pure I/O helpers for
signing keys, api.tls cert/key, and email template/notification content path
resolution. legacyResolveLocalConfigValues now accumulates inputs across its
existing value-derivation order and calls the shared validator once at the
end; the 3 file reads stay inline at their original position. D
(legacy-db-config.toml-read.ts) is unchanged and still wired up in a
follow-up commit.
…ng db.major_version=0 parity

legacyReadDbToml now builds a LegacyConfigValidationInput/LegacyAuthInput adapter and
runs every Go Config.Validate branch it shares with legacy-local-config-values.ts (db
port/major_version, storage bucket names, function slugs, edge_runtime.deno_version,
analytics backend/bigquery fields, experimental.pgdelta.format_options, and the auth
site_url/captcha/passkey/hooks/mfa/email-smtp/third_party sequence) through the single
shared legacyValidateResolvedConfig call, instead of duplicating that logic inline.

This fixes the db.major_version = 0 divergence: it previously fell through to the
generic "Failed reading config: Invalid db.major_version: 0." message instead of Go's
"Missing required field in config: db.major_version".

auth.sms and auth.external stay 100% inline in D (never part of the shared validator,
per legacy-config-validate.ts's module header) and now run after the shared call
succeeds, still gated on auth.enabled.
…y check

Move every pure legacyValidateResolvedConfig behavioral test (db.major_version,
storage.buckets, edge_runtime.deno_version, analytics, experimental.*, auth.site_url/
captcha/passkey/hooks/mfa/smtp/third_party, functions.*, api.tls presence) out of
legacy-local-config-values.unit.test.ts and into legacy-config-validate.unit.test.ts as
direct legacyValidateResolvedConfig calls, since both D and L now share that single
validator and no longer need per-caller duplication of this coverage. Env-override
mechanics, file I/O, and ProjectConfig-schema-specific behavior stay in
legacy-local-config-values.unit.test.ts unchanged.

Add direct regression coverage for the db.major_version=0 fix (0c62a91) and the
auth.captcha.provider decode-time enum check at the shared-validator level, a new test
for L's auth.email.template content/content_path exclusivity divergence gained in
49fc64a, and a dedicated legacy-config-validate.parity.unit.test.ts that drives both
D's and L's real pipelines with equivalent misconfigurations and asserts they fail with
the same Go-parity error message. Also updates legacy-config-validate.ts's stale
module-header status note now that both D and L are fully wired through it.
…us-commands

# Conflicts:
#	apps/cli/docs/go-cli-porting-status.md
#	apps/cli/src/legacy/commands/seed/buckets/buckets.handler.ts
…ets hoist merge

develop's db push/reset/start port (#5715) hoisted the seed-buckets
config load out of buckets.handler.ts and into the shared
legacySeedBucketsRun (also used by db reset --local), which
superseded the inline goViperCompat opt-in added on this branch.
Carry the opt-in over to the new call site so seed buckets and
db reset --local keep Go-parity config semantics.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ddbeb555a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +833 to +835
const hook = config.auth.hook[hookType];
if (hook.enabled) {
hooks.push({ type: hookType, uri: hook.uri ?? "", secrets: hook.secrets ?? "" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply auth hook env overrides before validation

When auth is enabled and a nested override such as SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED=true is supplied from the shell or dotenv without a matching URI, Go applies it through Viper ExperimentalBindStruct/AutomaticEnv before Config.Validate, so hook.validate fails with the missing auth.hook.send_email.uri before status or the default stop path reaches Docker. This loop only inspects the TOML-decoded config.auth.hook[...], so an env-only enabled hook remains disabled and native status/stop can accept a config the Go CLI rejects; apply the nested SUPABASE_AUTH_HOOK_* overrides before building the hook validation inputs.

Useful? React with 👍 / 👎.

Comment on lines +787 to +789
enabled: config.auth.captcha.enabled ?? false,
provider: config.auth.captcha.provider,
secret: config.auth.captcha.secret,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply CAPTCHA env overrides before validation

When auth is enabled and a config has [auth.captcha] enabled = false, an environment override like SUPABASE_AUTH_CAPTCHA_ENABLED=true makes Go validate that same block and fail if provider or secret is missing before status/default stop touches Docker. This input is built directly from the decoded TOML values, so the override is ignored and the disabled CAPTCHA block is accepted; resolve SUPABASE_AUTH_CAPTCHA_* before passing the CAPTCHA fields into the Go-parity validator.

Useful? React with 👍 / 👎.

const passkeyDoc = asRecord(authDocument?.["passkey"]);
const webauthnDoc = asRecord(authDocument?.["webauthn"]);
const passkey: LegacyPasskeyInput | undefined =
passkeyDoc?.["enabled"] === true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply passkey env overrides before validation

When auth is enabled and [auth.passkey] enabled = false is overridden by SUPABASE_AUTH_PASSKEY_ENABLED=true, Go validates the now-enabled passkey config and rejects it if the required [auth.webauthn] fields are absent. This branch only checks the raw TOML document's enabled value, so the env override is missed and native status/default stop can continue with a config Go rejects; apply the SUPABASE_AUTH_PASSKEY_*/SUPABASE_AUTH_WEBAUTHN_* overrides before building passkey.

Useful? React with 👍 / 👎.

Comment on lines +870 to +873
enabled: smtpDoc["enabled"] === undefined ? true : smtpDoc["enabled"] === true,
host: typeof smtpDoc["host"] === "string" ? smtpDoc["host"] : "",
port: typeof smtpDoc["port"] === "number" ? smtpDoc["port"] : 0,
user: typeof smtpDoc["user"] === "string" ? smtpDoc["user"] : "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply SMTP env overrides before validation

When [auth.email.smtp] enabled = false is present and the shell or dotenv sets SUPABASE_AUTH_EMAIL_SMTP_ENABLED=true without the required SMTP fields, Go applies that override before Auth.Email.validate and fails config loading. This code derives enabled and the SMTP fields only from smtpDoc, so native status/default stop can accept the explicitly disabled TOML block even though Go sees it as enabled; include the nested SUPABASE_AUTH_EMAIL_SMTP_* overrides in this input.

Useful? React with 👍 / 👎.

Comment thread packages/config/src/io.ts Outdated
}
}

const config = yield* parseProjectConfig(normalizedForDecode, format, filePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unknown Go config keys before decode

When goViperCompat callers load a config.toml with an unknown Go key such as a stray top-level foo = "bar" or an unsupported nested field, Go's v.UnmarshalExact rejects the file before status or the default stop path reaches Docker. This decode path uses ProjectConfigSchema, which this file notes ignores excess properties, so those keys are silently dropped and the native commands can run with a config the Go CLI refuses; add an exact raw-document key check for Go-parity loads before parsing into the shared schema.

Useful? React with 👍 / 👎.

// block and the functions loop (`pkg/config/config.go:1158-1173`), and —
// unlike `studio.port`/`local_smtp.port` above — unconditionally, with no
// `edge_runtime.enabled` gate.
const denoVersion = envOverrideDenoVersion(config.edge_runtime.deno_version, projectEnvValues);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply remaining typed env overrides before validation

When SUPABASE_EDGE_RUNTIME_POLICY=bogus or SUPABASE_ANALYTICS_PORT=not-a-port is supplied from the shell or dotenv, Go applies it through Viper ExperimentalBindStruct/AutomaticEnv before UnmarshalExact, so the RequestPolicy enum or uint16 port decode fails before status/default stop reaches Docker. This resolver only applies the edge_runtime enabled/deno_version and analytics enabled/backend/GCP overrides, so those remaining typed SUPABASE_* overrides are ignored and native commands can accept configs Go refuses; validate all typed env-overridden fields, even ones status does not render.

Useful? React with 👍 / 👎.

Coly010 added 5 commits July 8, 2026 10:28
…us-commands

# Conflicts:
#	apps/cli/src/legacy/commands/secrets/set/set.handler.ts
#	packages/config/src/io.ts
…e quality)

Collapses a .pipe() call back to oxfmt's canonical single-line form;
introduced by the develop merge conflict resolution and left unformatted.
…ting scheme (review: #PRRT_kwDOErm0O86OsZMu)

Go's hookConfig.validate calls url.Parse before the scheme switch
(pkg/config/config.go:1497-1499) and rejects a malformed URI (e.g. an
unterminated IPv6 host like http://[::1) before status/stop reach Docker.
The TS validator only regex-extracted a scheme prefix, so any http:/https:
prefix was accepted regardless of the rest of the URI. Reuses the existing
legacyGoUrlParse port (already used for studio.api_url) instead of the
scheme regex.
…PI keys (review: #PRRT_kwDOErm0O86OrzAU)

Go's config.Secret fields (auth.jwt_secret/publishable_key/secret_key/
anon_key/service_role_key, pkg/config/auth.go:181-185) are decrypted by
DecryptSecretHookFunc unconditionally during UnmarshalExact
(pkg/config/secret.go:30-73), failing config load on an undecryptable
value before status/stop continue. @supabase/config's schema only tags
these fields for later Redacted wrapping and never decrypts them, so the
status/stop resolver was using an "encrypted:..." value as literal (wrong)
key material, and never failed on an undecryptable one. Reuses the
existing eciesjs-based decrypt primitives from legacy-vault-decrypt.ts
(already used by the db/migration config loader for the same Go hook).
…ection (review: #PRRT_kwDOErm0O86Ossk3)

Go's -o/--output is a root PersistentFlags() enum (cmd/root.go:330) every
subcommand inherits, so "stop -o csv"/"-o table" is rejected by pflag at
parse time even though stop.RunE never reads the value itself. The doc
claimed Go has no such flag and that TS accepting an unknown value was a
harmless divergence — in fact stop.command.ts already rejects the same
values via withLegacyCommandInstrumentation's default outputFormats gate,
matching Go exactly. No behavior change, just correcting the doc.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c6d766f35

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +189 to +192
const loadedConfig = yield* loadProjectConfig(runtimeInfo.cwd, {
projectRef: ref,
goViperCompat: true,
}).pipe(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Swallow invalid remote config errors for secrets set

When config.toml contains a malformed [remotes.*].project_id, enabling goViperCompat here makes loadProjectConfig throw InvalidRemoteProjectIdError, but the tolerant secrets set path only catches parse/env/duplicate errors below. Go’s internal/secrets/set/set.go logs any flags.LoadConfig error and still uploads secrets supplied via --env-file or positional NAME=value, so this can now abort a valid secret update before the explicit secrets are sent; handle the invalid-remote error the same way as the duplicate-remote error.

Useful? React with 👍 / 👎.

Comment on lines +278 to +280
const failedStop = stopResults.find(
(result) => Result.isFailure(result) || result.success !== 0,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore already-stopped containers during stop

When a labeled Supabase container is already exited (for example after a prior crash or manual docker stop), this treats any non-zero docker stop <id> result as fatal and returns before the prune steps. The Go implementation lists All: true too, but explicitly ignores Docker's errdefs.IsNotModified from ContainerStop, so the same stale stopped container is pruned successfully; ignore the CLI's not-running failure instead of aborting the whole stop.

Useful? React with 👍 / 👎.

Comment thread packages/config/src/io.ts
Comment on lines +715 to +716
const { document: normalizedForDecode, deprecatedProviders } =
normalizeDeprecatedExternalProviders(documentForDecode);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Slack provider unless Go parity is requested

When a non-Go caller loads a config containing the previously supported [auth.external.slack] block without goViperCompat, this unconditional normalization deletes that provider before schema decoding while the warning is gated off below. The new option is documented as preserving pre-PR behavior for next/, packages/stack, and other default callers, but those paths now silently lose the configured Slack provider instead of retaining or warning about it; gate this stripping behind goViperCompat or keep the old field for default loads.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant