feat(cli): generate types natively against the selected stack (CLI-2366) - #6652
Conversation
`gen types` no longer spawns a pg-meta container. Introspection and code generation run in-process through `@supabase/postgrest-typegen` over a direct PostgreSQL connection, so `--local` works against a stack whose runtime is native rather than Docker — previously impossible, since the stack has no pg-meta capability and a native stack has no Docker daemon to run one in. Generation goes through the shared `DbConnection` seam, which already owns TLS mode, DoH resolution, pooler fallback and connect-error classification, so no second connection path exists. The SSL probe is gone: linked, project-ref and preview-branch targets are known Supabase hosts and pin the bundled CA with `sslmode=require`; `--db-url` honours the DSN's own `sslmode`/`sslrootcert` and otherwise pins only for known Supabase hosts; `--local` stays plaintext. `--query-timeout` now maps to a server-enforced `statement_timeout`. `--network-id` is rejected on this command: in-process generation cannot join a Docker network. Both argv positions are covered, including the persistent form before the command path. Output is byte-identical to pg-meta v0.99.0 for Go and Swift. TypeScript types a NOT NULL jsonb column as `NonNullable<Json>` rather than `Json`, and Python uses pydantic's `JsonValue` rather than `Json[Any]`; both are upstream corrections and are documented in the command's SIDE_EFFECTS.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🤖 AI Review
Verified all 14 reported findings and merged them into 11 distinct entries. Nine are confirmed and two are refuted by documented existing behavior or repository conventions. The most significant confirmed issue is the immediate connection failure caused by timeouts that round to zero.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🟠 MAJOR | apps/cli/src/commands/gen/types/types.handler.ts:296 |
timeout-handling |
claude+codex | Accepted timeout values below 500ms round to zero, causing the connection probe to time out immediately and disabling PostgreSQL's statement timeout. |
| 🟡 MINOR | apps/cli/src/commands/gen/types/types.oxfmt.ts:71 |
platform-support |
claude+codex | Source runs and build:binary builds on musl Linux select the incompatible glibc oxfmt binding. |
| 🟡 MINOR | apps/cli/src/commands/gen/types/SIDE_EFFECTS.md:17 |
documentation |
claude | The side-effect contract omits configuration, dotenv, libpq environment, and conditional service-file inputs now used by the --db-url path, including when schemas are explicit. |
| 🟡 MINOR | apps/cli/src/commands/gen/types/types.handler.ts:499 |
compatibility |
claude | The command now hard-fails whenever the persistent --network-id flag is present, breaking callers that previously used or globally supplied that supported flag. |
| 🟡 MINOR | apps/cli/src/commands/gen/types/types.generator.layer.ts:44 |
cancellation |
claude | Each introspection query is run in a separate Promise-backed root fiber, so interrupting generation does not interrupt the in-flight session query before the enclosing scope closes its pool. |
| 🟡 MINOR | apps/cli/src/command-internal/connect-errors.ts:375 |
error-message |
codex | The TLS failure suggestion tells users to append syntax that is invalid for URLs with existing query parameters and for keyword-style connection strings. |
| ⚪ NIT | apps/cli/tsconfig.types.json:6 |
documentation |
claude+codex | The type-check overlay points readers to a nonexistent generator integration test. |
| ⚪ NIT | apps/cli/src/commands/gen/types/types.integration.test.ts:850 |
comments |
codex | The newly added test section banners violate the repository's trusted comment convention. |
| ⚪ NIT | apps/cli/src/command-internal/db-connection.sql-pg.layer.ts:684 |
documentation |
claude | The CA-loading comment describes only sslrootcert file loading and omits the new higher-precedence inline CA branch and its different gate. |
Findings outside the diff
- ⚪ NIT
apps/cli/src/command-internal/db-connection.sql-pg.layer.ts:684— The CA-loading comment describes only sslrootcert file loading and omits the new higher-precedence inline CA branch and its different gate.
Refuted findings (kept for transparency, not posted as review comments)
apps/cli/src/commands/gen/types/types.handler.ts:295(timeout-handling): The default query-timeout unintentionally overrides timeout settings supplied through the DSN, service configuration, or environment.
Refuted: This is the command's established timeout contract, not an accidental override. trusted/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md:150 documents a default 15-second maximum, and the previous implementation unconditionally passed that value as both PG_CONN_TIMEOUT_SECS and PG_QUERY_TIMEOUT_SECS at trusted/apps/cli/src/commands/gen/types/types.handler.ts:456-457. The new documentation likewise explicitly states that the default applies as both connection and statement timeouts at apps/cli/src/commands/gen/types/SIDE_EFFECTS.md:171-176.apps/cli/src/commands/gen/types/types.handler.ts:96(correctness): isPoolerHost is accidentally broader than the repository's intended pooler-domain validation because it accepts any subdomain under the configured registrable domain.
Refuted: The repository defines poolerHost specifically as the required eTLD+1 ownership boundary at apps/cli/src/command-internal/profile.ts:18-23, and the existing pooler connection validator deliberately performs the same getDomain comparison at apps/cli/src/command-internal/db-config.parse.ts:405-413. Matching subdomains is therefore the documented convention.
Stats
Claude findings: 9 · Codex findings: 5 · Confirmed: 9 · Refuted: 2 · Uncertain: 0
Models: claude-opus-5 + gpt-5.6-sol · Trigger: auto · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
A sub-second `--query-timeout` was unusable: the duration parser rounded to whole seconds, so `400ms` became `0`, which disabled `statement_timeout` server-side and collapsed the connect timeout to `Duration.seconds(0)` — the connection could never be established. The rounding only existed to satisfy pg-meta's integer-seconds environment contract, so the parser now returns milliseconds and the connection derives an exact `statement_timeout` and a connect timeout of at least one second. An explicit `0s` still disables the bound and leaves the driver's own connect default in place. Interrupting generation now aborts the in-flight introspection query, by forwarding `Effect.tryPromise`'s signal into the Promise bridge rather than leaving each query on a detached root fiber. The oxfmt binding falls back to runtime libc detection when the `SUPABASE_LIBC` build define is absent, so source runs and development binaries on musl no longer select the glibc binding. The TLS suggestion no longer prescribes `?`, which is wrong for a DSN that already carries a query string and for keyword-style connection strings. `SIDE_EFFECTS.md` now records the config, dotenv, libpq service file and `PG*` inputs that `--db-url` reads through the shared resolver. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jgoux
left a comment
There was a problem hiding this comment.
Two verified regressions remain after independent review and cross-review.
avallete
left a comment
There was a problem hiding this comment.
issue (blocking): Three regressions remain on 5b15cdd91
In-process --local generation dogfoods clean against the stack-engine matrix: compose, docker eager, docker lazy, and native all exit 0; native still works with Docker off PATH (no pg-meta container); --network-id rejects; --query-timeout 400ms succeeds; staging --linked TypeScript (Management API) and Go (direct connect) succeed.
The remaining blockers are inline: @jgoux's two [P2]s (pooler classification dropping driver cause/code/address, and pnpm cli-release missing the oxfmt externals) plus one more — --db-url classified as local silently ignores explicit sslmode.
…nals (CLI-2366) Routing generation through `DbConnection` lost the IPv4 pooler retry on IPv4-only networks. `toConnectError` builds `DbConnectError` from a rendered message and drops the driver error, so the structured errno classifier could no longer see `ENOTFOUND`, or `EHOSTUNREACH`/`EADDRNOTAVAIL` against an IPv6 address, and the direct-host failure never fell back to the pooler. The boundary now classifies the failure while the driver error is still in scope and carries the verdict on `DbConnectError.ipv6Unreachable`, the way `retryable` already works, rather than attaching a raw driver error to a domain error. The fallback tests are built from errors the connection layer actually produces instead of hand-shaped ones. `tools/release/local-release.ts` compiles the CLI through its own `bun build` invocation, which did not mark oxfmt's optional prettier imports external and so could not compile a local release. It now shares the same externals list as the other two build paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…I-2366) `sslConfigsFor` exempts a local target from TLS before it reads `sslmode`, so a DSN pointing at a TLS tunnel on loopback with `sslmode=require` or `verify-full` was silently downgraded to plaintext, and the root cert was never loaded because the CA derivation is gated the same way. `gen types --db-url` is documented as honoring the DSN's own TLS settings, and an explicitly requested verification must not be dropped. The acquisition path now treats a connection that names `sslmode`, `sslrootcert` or an inline CA as TLS-exempt only when it is also not local: `sslConfigsFor`'s own contract is unchanged, it just receives the caller's exemption decision rather than the raw classification. A loopback DSN that asks for nothing still connects in plaintext, and only DSN parsing ever populates these fields, so ordinary `--local` connections are unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
All three blockers from @avallete's review are now addressed, plus the earlier AI-review round.
Two of these were real regressions introduced by routing generation through @avallete — thank you for the dogfooding matrix; that is far better evidence than anything I could produce locally, particularly native-with-Docker-off- One item from the AI round remains open by choice rather than oversight: both bots wanted Also unchanged and worth a second opinion before merge: the jsonb output deltas in the description. Go and Swift stay byte-identical; TypeScript and Python shift on jsonb columns because the library corrected two upstream bugs. |
avallete
left a comment
There was a problem hiding this comment.
issue (blocking): tlsExplicitlyRequested is a regression on local --db-url
The follow-up in 967878c85 correctly stopped ignoring sslmode=require / verify-full on a loopback TLS tunnel. The predicate it added is too broad: any populated sslmode lifts the local plaintext exemption, including libpq prefer and a PGSSLMODE fill-in the DSN never set. That makes a previously working local --db-url fail TLS against plaintext Postgres, and it is on the shared DbConnection seam so db query / dump / pull are in the blast radius too.
Inline: how to reproduce and the smallest fix.
| export function tlsExplicitlyRequested(cfg: PgConnInput): boolean { | ||
| return ( | ||
| cfg.sslmode !== undefined || | ||
| (cfg.sslrootcert?.length ?? 0) > 0 || | ||
| (cfg.sslrootcertInline?.length ?? 0) > 0 | ||
| ); |
There was a problem hiding this comment.
issue (blocking): this is a regression of local --db-url (and every other DbConnection command), not just a too-eager helper.
967878c85 was meant to honor an explicit TLS demand on a target classified local — a TLS tunnel on 127.0.0.1 with sslmode=require / verify-full / sslrootcert. That part is right: before this commit, sslConfigsFor returned [false] whenever isLocal was true, so the DSN’s TLS settings never reached the wire.
The predicate as written treats any defined sslmode as that demand:
cfg.sslmode !== undefined || sslrootcert || sslrootcertInlineThat is not the same as “the DSN asked for TLS”. Two realistic inputs now lift the exemption and then hit TLS-only dialing (sslConfigsFor maps prefer / unset to TLS with no plaintext fallback, isLocal && !explicitTls at the acquisition site):
?sslmode=preferon a loopback DSN. libpqprefermeans try TLS, then plaintext. This CLI has never implemented that fallback —preferis TLS-only — but local classification previously forced plaintext before that mapping ran. After this commit,preferis “explicit TLS” and the local Postgres that does not speak SSL fails with a handshake error instead of connecting.PGSSLMODE(or a service-filesslmode) with nosslmodein the DSN.parseConnectionStringcopies those intocfg.sslmodewhen the URL omits it (db-config.parse.ts, URL and keyword parsers). A developer withPGSSLMODE=prefer(orrequire) in the environment, thengen types --db-url postgresql://postgres:postgres@127.0.0.1:54322/postgres, now fails against the local stack. The same path is used bydb query --db-urland anything else that goes throughacquirePgPoolConnection.
That is a behavior change from 5b15cdd91 / pre-exemption-fix: a classified-local DSN with no TLS keys in the URL connected in plaintext. It is also wider than the bug that was filed — a missing sslmode=require on a TLS loopback tunnel. sslmode=disable happens to still work only because sslConfigsFor special-cases it after the exemption is already gone.
Dogfood on this SHA confirmed the intended tunnel case: compose and native loopback with a bare DSN and with sslmode=disable still connect; sslmode=require against plaintext Postgres now fail-closes (SSLRequest on the wire). The hole is the prefer / env-filled sslmode case, which this helper cannot distinguish from require.
Smallest fix: treat TLS as explicitly requested only for require / verify-ca / verify-full, or when sslrootcert / an inline CA is set. Leave prefer / allow / unset / env-filled prefer on a local target as plaintext. That keeps the TLS-tunnel fix and restores the local --db-url that used to work.
There was a problem hiding this comment.
You are right, and this landed before I saw the review — follow-up in #6664.
Your diagnosis is exact, and the PGSSLMODE half is the part I got wrong twice: I noted the ambient-env risk while designing the predicate, convinced myself it was acceptable because libpq would also honour it, and missed that prefer is the common ambient value and that this CLI maps prefer to TLS-only with no fallback. Treating "has an sslmode" as "demands TLS" conflated a fallback description with a demand.
#6664 narrows it to require / verify-ca / verify-full, or a supplied sslrootcert / inline CA — your smallest fix.
Reproduced the regression against a plaintext Postgres on the local-classified port, before and after, same database:
| Connection | merged develop |
#6664 |
|---|---|---|
| bare loopback DSN | connects | connects |
PGSSLMODE=prefer |
fails | connects |
PGSSLMODE=allow |
fails | connects |
?sslmode=prefer |
fails | connects |
?sslmode=disable |
connects | connects |
?sslmode=require |
fails closed | fails closed |
PGSSLMODE=verify-full |
fails closed | fails closed |
The tunnel case from the previous round is unchanged — an explicit demand still puts an SSLRequest on the wire against a loopback target, and a root cert still opens the CA gate.
On test coverage: the unit tests I shipped last round actively asserted the broad behaviour (including sslmode=disable counting as a demand), so they encoded the bug rather than catching it. Those assertions are corrected, and there is now a test that runs the real DSN parser with a PGSSLMODE env fill-in into the predicate, so the env-sourced case is covered end to end instead of reasoned about.
|
Merged before @avallete's second review landed — the regression it identifies is real and is fixed in #6664.
|
…66) (supabase#6664) ## Summary Follow-up to supabase#6652, addressing @avallete's post-merge review ([review](supabase#6652 (review))). supabase#6652 stopped `sslConfigsFor` from silently downgrading an explicit `sslmode=require` / `verify-full` to plaintext on a target classified local. The predicate it introduced was too broad: it treated **any** populated `sslmode` as a TLS demand. That is not the same as the DSN asking for TLS: - `prefer` and `allow` name a TLS/plaintext fallback that libpq performs and `sslConfigsFor` deliberately does not — it maps `prefer` and unset to TLS-only. Local classification used to force plaintext *before* that mapping ran, so reading `prefer` as a demand turns a plaintext-capable target into a handshake failure. - `sslmode` is also filled from `PGSSLMODE` and libpq service files when the DSN omits it, so a merely present value can come from the environment rather than the connection string. The result was a regression on the shared `DbConnection` seam: a developer with `PGSSLMODE=prefer` exported, or a DSN carrying `?sslmode=prefer`, could no longer reach a local plaintext Postgres — affecting `db query --db-url`, dump and pull as well as `gen types`. Only `require`, `verify-ca`, `verify-full`, or a supplied `sslrootcert` / inline CA now count as a demand. ### Verified against a plaintext Postgres on the local-classified port | Connection | merged `develop` | this branch | | --- | --- | --- | | bare loopback DSN | connects | connects | | `PGSSLMODE=prefer` | **fails** | connects | | `PGSSLMODE=allow` | **fails** | connects | | `?sslmode=prefer` | **fails** | connects | | `?sslmode=disable` | connects | connects | | `?sslmode=require` | fails closed | fails closed | | `PGSSLMODE=verify-full` | fails closed | fails closed | The TLS-tunnel behaviour supabase#6652 set out to fix is unchanged: an explicit demand still puts an SSLRequest on the wire against a loopback target, and a supplied root cert still opens the CA gate. Coverage is behavioural rather than shape-only — the integration tests assert whether an SSLRequest actually reaches a fake Postgres server, and a unit test composes the real DSN parser with the predicate so the `PGSSLMODE`-filled case is covered end to end rather than assumed. ## Linked issue Follow-up to [CLI-2366](https://linear.app/supabase/issue/CLI-2366) (no GitHub issue). - [x] The linked issue is **open** and carries the `open-for-contribution` label (or I'm a Supabase maintainer). ## Checklist - [x] The PR title follows [Conventional Commits](https://www.conventionalcommits.org/) (e.g. `fix(cli): …`). - [x] Tests added or updated for the change. - [x] From the repository root, `pnpm check:all` passes; relevant package tests pass for every touched workspace, and `pnpm types:check` passes for each touched TypeScript workspace (or workspace declaring it). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Summary
gen typesno longer spawns apostgres-metacontainer. Introspection and code generation runin-process through
@supabase/postgrest-typegenover a direct PostgreSQL connection.
This is what
--localon the managed stack needs: the stack exposes no pg-meta capability, and itsupports a native runtime with no container engine at all, so the old path required a Docker daemon
to generate types for a database that isn't in Docker. #6540 already resolved the stack's real
endpoint here; this removes the container that resolution still fed.
Generation goes through the shared
DbConnectionseam thatdb queryuses, so there is no seconddriver path — TLS mode, DoH resolution, pooler fallback and connect-error classification are the
ones already in place.
TLS. The SSL probe is gone. Linked, project-ref and preview-branch targets are known Supabase
hosts and pin the bundled CA with
sslmode=require, preserving the stricter posturegen typesalready had.
--db-urlhonours the DSN's ownsslmode/sslrootcertand otherwise pins only forknown Supabase hosts;
--localstays plaintext.PgConnInputgains an optional inline-CA field sopinning needs no temp file.
--query-timeoutnow maps to a server-enforcedstatement_timeoutrather than a container envvar, so a stalled introspection is aborted by the server rather than only client-side.
Behaviour changes
--network-idis rejected on this command. In-process generation cannot join a Docker network.Both argv positions are covered, including the persistent form before the command path.
A
--db-urlpointing at a plain-TCP server now needs?sslmode=disable. The shared connectionlayer intentionally does not downgrade from
preferto plaintext, andgen typesno longer probes.This matches every other
--db-urlcommand; a connect-error suggestion names the fix.Generated output, measured against
postgres-metav0.99.0 on the same schema:NOT NULLjsonb column isNonNullable<Json>rather thanJsonJsonValuerather thanJson[Any];NotRequired/TypeAliasimport fromtyping_extensionsBoth differences are upstream corrections: the old TypeScript type admitted
nullfor aNOT NULLcolumn, and pydantic's
Json[Any]expects an unparsed JSON string rather than a decoded rowvalue.
--linked/--project-idTypeScript is generated server-side by the Management API and isunaffected, so it can differ from local output on these jsonb cases until the hosted service adopts
the same engine. Recorded in the command's
SIDE_EFFECTS.md.Credit
Supersedes #6404 by @avallete, which first proved this out and worked through the
bun build --compileproblems withoxfmt— the optional-plugin externals and the staticallyembedded napi binding here are that PR's design. Restarted from
developbecause #6525 moved everypath it touched.
Linked issue
Resolves CLI-2366. Also covers
CLI-2279 (no GitHub issue).
open-for-contributionlabel (or I'm a Supabase maintainer).Checklist
fix(cli): …).pnpm check:allpasses; relevant package tests pass for every touched workspace, andpnpm types:checkpasses for each touched TypeScript workspace (or workspace declaring it).🤖 Generated with Claude Code