feat(cli): port supabase start command to native TypeScript#5847
feat(cli): port supabase start command to native TypeScript#5847Coly010 wants to merge 31 commits into
Conversation
Ports `supabase start` from a Go-binary proxy to native TypeScript in the legacy CLI shell (CLI-1323), continuing the local-dev-stack shell migration alongside the already-ported `stop`/`status`/`db push`/`db reset`/`db start`. ## What changed Talks directly to Docker/Podman via subprocess, mirroring Go's sequential per-container `DockerStart` — no Docker Compose, and no `@supabase/stack/effect` orchestration (that runtime targets a deliberately different local-dev product and would silently manage the wrong set of containers). Brings up all 14 containers in Go's real start order, with per-service config-boolean + `--exclude` gating, Go-byte-exact env/image resolution, a bulk health-check phase (Docker healthcheck for most services, an HTTP-HEAD-through-Kong bypass for PostgREST/Edge Runtime), and full rollback on any bring-up failure. Also natively implements the fresh-volume DB schema/migration/seed pipeline, Edge Runtime container bring-up, and fresh-volume storage bucket seeding — previously tracked as out-of-scope follow-ups for this port, now closed. Only the linked-project version-check suggestion (a best-effort "update available" hint with zero Management API dependency otherwise) remains out of scope by design. ## Notable review-driven fixes - `legacyParseGoDuration` (the `config.toml` duration-string parser feeding Go-parity env vars like `GOTRUE_SESSIONS_TIMEBOX`) silently accepted a malformed duration with no digits (a bare unit like `"s"`, or a lone `"."` with no digits on either side) and returned `0` instead of erroring like Go's real `time.ParseDuration` — both cases now throw Go's exact `time: invalid duration "..."` message. - `start.services.ts`'s descriptive `enabledGate` metadata for Mailpit referenced the deprecated `inbucket` config section instead of its `local_smtp` rename. Fixed, and added a mechanical cross-check test that evaluates every service's `enabledGate` string against `start.gates.ts`'s real computed gate across a battery of synthetic configs, so a future drift between the two fails loudly instead of silently. - Confirmed and closed the PostgREST health-check's TLS/CA trust gap for `[api.tls] enabled = true` local stacks (self-signed Kong cert) — the local-Kong-CA-trust mechanism already used by `seed buckets`/`storage`/ `db reset` is now wired into `start`'s health-check HTTP client too. Closes CLI-1323
|
👋 Thanks for the contribution! This pull request isn't linked to a tracked issue, so it's being closed automatically. Please open an issue first, wait for a maintainer to add the |
…3-port-supabase-start
|
👋 Thanks for the contribution! This pull request isn't linked to a tracked issue, so it's being closed automatically. Please open an issue first, wait for a maintainer to add the |
|
👋 Thanks for the contribution! This pull request isn't linked to a tracked issue, so it's being closed automatically. Please open an issue first, wait for a maintainer to add the |
|
👋 Thanks for the contribution! This pull request isn't linked to a tracked issue, so it's being closed automatically. Please open an issue first, wait for a maintainer to add the |
) The contribution gate identified internal maintainers solely from the PR's `author_association`, which GitHub only reports as `MEMBER` when a user's organization membership is public. A private org member (e.g. a `supabase/cli` team member who keeps membership private) is reported as `CONTRIBUTOR`/`NONE`, so the gate wrongly closed their PRs as "no-linked-issue" (see supabase#5847). Resolve maintainer status from the author's effective repository permission (`admin`/`write`), which reflects team/org-granted access that `author_association` does not surface, falling back to it only when the cheap signals (bot, public internal association) are inconclusive. The permission endpoint needs just `Metadata: read`, already covered by the workflow's `contents: read`. Claude-Session: https://claude.ai/code/session_01D41gYiFBSU7adE4ppnUUKg --------- Co-authored-by: Claude <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 623dd8b18e
ℹ️ 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".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 623dd8b18e
ℹ️ 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".
supabase start's TS port had five spots where Go's real behaviour was
silently dropped:
- supabase/.temp/storage-migration (the linked project's Storage
migration pin) was never read, so DB_MIGRATIONS_FREEZE_AT was always
empty on a fresh DB setup and the Storage container itself.
- supabase/.temp/{gotrue,rest,storage,realtime,studio,pgmeta,logflare,
pooler}-version pins were never applied to the images start
pulls/creates, unlike Go's Config.Load rewrite. Hoisted the existing
services command's reader into shared/legacy-service-version-overrides.ts
and reused services.shared.ts's tag-rewrite helpers instead of a third
copy.
- --network-id was read by several other native ports but never by
start itself, so the override never reached any container or the
Docker network start creates.
- SUPABASE_API_PORT's env override was computed for status URLs but
never exposed for Kong/Edge Runtime's own container specs, so they
kept publishing/binding the un-overridden config.api.port.
- Studio's function bind mounts were hardcoded to [], unlike Go's
unconditional (Edge-Runtime-independent) PopulatePerFunctionConfigs
call — extracted the existing per-function bind computation in
shared/functions/serve.ts into a reusable resolveFunctionBindMounts
so both callers share one calculation.
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…l in start Three more Go-parity gaps in supabase start found by review: - db.root_key (including encrypted: values) was never resolved or passed to the Postgres container, so every project booted with the hard-coded default pgsodium key instead of a customized one, breaking decryption of existing encrypted data. Resolved in legacyResolveLocalConfigValues off the raw config document (unmodeled in @supabase/config's schema), reusing the existing decrypt helpers. - DockerStart's Linux-only host.docker.internal:host-gateway mapping was already correctly ported for the one-shot migrate jobs and Edge Runtime bring-up, but never made it into the common legacyStartContainer path the other 13 services go through. - auth.external_url (also unmodeled in the schema, with its own Go regression test) was never read, so GoTrue's API_EXTERNAL_URL/JWT issuer default/mailer verify URL/OAuth redirect fallbacks always derived from apiUrl even when a project intentionally exposed auth at a different host.
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…es tag ordering Three more Go-parity gaps found by review: - [auth.email.smtp] present without an explicit enabled key should default to enabled (Go sets this at load time), but the schema always decodes enabled: false when the key is absent, so GoTrue silently fell back to Mailpit. Reuses the existing correct resolution already implemented for config validation, exposed as legacyResolveAuthEmailSmtp. - auth.external providers outside the schema's fixed ~19-provider set (e.g. a custom [auth.external.my_oidc] block) never reached GoTrue's env, even though Go's Auth.External is a genuine map iterated unconditionally and this port's own config validation already accepts arbitrary provider names. Reuses the same raw-document iteration validateAuthExternalProviders already established. - A registry override with a port (SUPABASE_INTERNAL_IMAGE_REGISTRY= localhost:5000) broke the Postgres version-tag comparison, since Go compares the pre-registry-rewrite image while this port was comparing the already-rewritten one. Threaded the original image through as a separate configImage field used only for that comparison.
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3e5cd58c9a
ℹ️ 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".
| Effect.ensuring( | ||
| Effect.suspend(() => | ||
| legacyCleanupStartSecrets(cliConfig.workdir, containerNamesBeforeTeardown), | ||
| ), |
There was a problem hiding this comment.
Avoid deleting secrets before teardown succeeds
When legacyDockerRemoveAll fails after the initial listing, this ensuring still deletes staged secret directories for every listed container. In scenarios such as docker stop failing for one container or container prune failing after stops, some containers can remain live or at least restartable with bind mounts pointing at these files, so a failed supabase stop can break the remaining stack by removing its TLS/JWT/pgsodium/pooler secrets. Limit this cleanup to containers confirmed removed, or run it only after teardown has succeeded.
Useful? React with 👍 / 👎.
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3e5cd58c9a
ℹ️ 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".
| export const legacyStartEdgeRuntimeContainer = Effect.fn("legacy.start.edgeRuntime")(function* ( | ||
| input: LegacyEdgeRuntimeBringUpInput, | ||
| ) { | ||
| return yield* startEdgeRuntimeContainer({ |
There was a problem hiding this comment.
Preserve Podman fallback for Edge Runtime start
When Docker is absent but Podman is installed, this default Edge Runtime path now delegates to startEdgeRuntimeContainer, whose setup/run/reload flow still calls helpers that hard-code runChildProcess("docker", ...) instead of the spawnContainerCli Docker/Podman fallback used by the rest of native start. Because edge_runtime.enabled defaults to true, supabase start can get through the earlier image/container steps via Podman and then fail as soon as it reaches Edge Runtime; thread the same container-CLI fallback through this path too.
Useful? React with 👍 / 👎.
| return legacyInspectDockerContextHost(spawner).pipe( | ||
| Effect.orElseSucceed(() => legacyPlatformDefaultDockerHost(platform)), |
There was a problem hiding this comment.
Resolve Vector's socket from the active container CLI
On a Podman-only host with analytics enabled (the default) and no DOCKER_HOST, the rest of native start can use the Podman fallback, but this resolver still probes only docker context inspect and then falls back to Docker's default /var/run/docker.sock. Vector then gets a bind mount for a Docker socket that does not exist instead of the Podman socket, so supabase start can fail at the Vector container (or start it without log access); derive this host from the same selected container runtime or add a Podman-aware socket fallback.
Useful? React with 👍 / 👎.
| const binds = await Promise.all( | ||
| secretFiles.map(async (secretFile, index) => { | ||
| const hostPath = join(dir, `secret-${index}`); | ||
| await writeFile(hostPath, secretFile.content, { mode: 0o600 }); |
There was a problem hiding this comment.
Keep staged secrets readable by container users
When these files are bind-mounted into Postgres/Kong/Supavisor on Linux, Docker preserves the host file mode and numeric owner; with 0600 owned by the invoking host user, non-root service users inside the images cannot read the mounted pgsodium key/Kong config/pooler script. The Go path wrote the same contents inside the container via the entrypoint heredoc, so they were readable there; keep the parent directory private but make the staged files container-readable, or copy/chown them before exec.
Useful? React with 👍 / 👎.
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3e5cd58c9a
ℹ️ 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".
| if (hours > 0) return `${sign}${hours}h${minutes}m${secs}s`; | ||
| if (minutes > 0) return `${sign}${minutes}m${secs}s`; |
There was a problem hiding this comment.
Preserve fractional seconds when formatting long durations
When a configured Go-style duration is at least one minute or hour and also has a fractional-second remainder, this returns before including the ms/us/ns portion. For example, auth.sms.max_frequency = "1m0.5s" or a session duration like "1h0.5s" is normalized to 1m0s/1h0m0s instead of Go's 1m0.5s/1h0m0.5s, so the GoTrue container receives a shorter timeout/rate-limit than the Go CLI would set.
Useful? React with 👍 / 👎.
| message: `failed to parse config: ${cause instanceof Error ? cause.message : String(cause)}`, | ||
| }), | ||
| }); | ||
| yield* legacyWaitForHealthyServices(spawner, [postgresContainerId], { |
There was a problem hiding this comment.
Honor --ignore-health-check for Postgres failures
With --ignore-health-check, this Postgres wait fails inside bringUp, so the surrounding rollback runs and the command exits non-zero before the later ignore-health-check downgrade is reached. Go applies ignoreHealthCheck && start.IsUnhealthyError(err) to the whole run() error, including StartDatabase's Postgres WaitForHealthyService, so a DB health timeout is supposed to be printed as a warning and still reach the normal status output; this regresses users who intentionally use the flag while Postgres is slow or unhealthy.
Useful? React with 👍 / 👎.
| @@ -576,7 +531,7 @@ const resolveAuthArtifacts = Effect.fnUntraced(function* ( | |||
| const shouldUseJwtSecretFallback = signingKeysPath.length === 0; | |||
|
|
|||
| const keys: unknown[] = []; | |||
| const issuerUrl = enabledThirdPartyIssuer(auth.third_party); | |||
| const issuerUrl = resolveThirdPartyIssuerUrl(auth.third_party); | |||
There was a problem hiding this comment.
Keep functions serve lenient when auth is disabled
When auth.enabled = false, Go skips Auth.ThirdParty.validate(), calls ResolveJWKS, and functions serve discards that error (jwks, _ := ...), so malformed or multiple third-party providers do not block local serving. This synchronous validating call happens before the remote-JWKS error swallowing, so configs such as disabled auth with an enabled WorkOS provider missing issuer_url or multiple enabled providers abort supabase functions serve instead of continuing with an empty JWKS.
Useful? React with 👍 / 👎.
|
|
||
| type Spawner = ChildProcessSpawner["Service"]; | ||
|
|
||
| const DOCKER_PULL_RETRY_DELAYS_MS = [500] as const; |
There was a problem hiding this comment.
Match Go's two pull retries for transient image pulls
Go calls DockerImagePullWithRetry(ctx, image, 2), which gives each registry candidate the initial pull plus two retries after longer backoffs. With a single 500 ms delay here, the port only makes one retry, so fresh starts fail on transient pull errors that would have succeeded on Go's second retry, especially when many service images are uncached.
Useful? React with 👍 / 👎.
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
What changed
Ports
supabase startfrom a Go-binary proxy to native TypeScript in the legacy CLI shell (CLI-1323), continuing the local-dev-stack shell migration alongside the already-portedstop/status/db push/db reset/db start.Talks directly to Docker/Podman via subprocess, mirroring Go's sequential per-container
DockerStart— no Docker Compose, and no@supabase/stack/effectorchestration (that runtime targets a deliberately different local-dev product and would silently manage the wrong set of containers). Brings up all 14 containers in Go's real start order, with per-service config-boolean +--excludegating, Go-byte-exact env/image resolution, a bulk health-check phase (Docker healthcheck for most services, an HTTP-HEAD-through-Kong bypass for PostgREST/Edge Runtime), and full rollback on any bring-up failure.Also natively implements the fresh-volume DB schema/migration/seed pipeline, Edge Runtime container bring-up, and fresh-volume storage bucket seeding — previously tracked as out-of-scope follow-ups for this port, now closed. Only the linked-project version-check suggestion (a best-effort "update available" hint with zero Management API dependency otherwise) remains out of scope by design.
Why
Continues the legacy-shell migration from Go-binary-proxy commands to native TypeScript, closing CLI-1323.
Reviewer-relevant context
legacyParseGoDuration(theconfig.tomlduration-string parser feeding Go-parity env vars likeGOTRUE_SESSIONS_TIMEBOX) silently accepted a malformed duration with no digits (a bare unit like"s", or a lone"."with no digits on either side) and returned0instead of erroring like Go's realtime.ParseDuration— both cases now throw Go's exacttime: invalid duration "..."message.start.services.ts's descriptiveenabledGatemetadata for Mailpit referenced the deprecatedinbucketconfig section instead of itslocal_smtprename. Fixed, and added a mechanical cross-check test that evaluates every service'senabledGatestring againststart.gates.ts's real computed gate across a battery of synthetic configs, so a future drift between the two fails loudly instead of silently.[api.tls] enabled = truelocal stacks (self-signed Kong cert) — the local-Kong-CA-trust mechanism already used byseed buckets/storage/db resetis now wired intostart's health-check HTTP client too.Note
This branch is currently 9 commits behind
develop(unrelateddeps/dockerbump commits) — opening as draft to get CI/review visibility; happy to rebase before this comes out of draft.