Skip to content

feat(cli): generate types natively against the selected stack (CLI-2366) - #6652

Merged
Coly010 merged 4 commits into
developfrom
columferry/cli-2366-cli-gen-types-local-use-the-selected-managed-stack
Sep 17, 2026
Merged

Coly010 merged 4 commits into
developfrom
columferry/cli-2366-cli-gen-types-local-use-the-selected-managed-stack

Conversation

@Coly010

@Coly010 Coly010 commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Summary

gen types no longer spawns a postgres-meta container. Introspection and code generation run
in-process through @supabase/postgrest-typegen
over a direct PostgreSQL connection.

This is what --local on the managed stack needs: the stack exposes no pg-meta capability, and it
supports 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 DbConnection seam that db query uses, so there is no second
driver 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 posture gen types
already had. --db-url honours the DSN's own sslmode/sslrootcert and otherwise pins only for
known Supabase hosts; --local stays plaintext. PgConnInput gains an optional inline-CA field so
pinning needs no temp file.

--query-timeout now maps to a server-enforced statement_timeout rather than a container env
var, so a stalled introspection is aborted by the server rather than only client-side.

Behaviour changes

--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.

A --db-url pointing at a plain-TCP server now needs ?sslmode=disable. The shared connection
layer intentionally does not downgrade from prefer to plaintext, and gen types no longer probes.
This matches every other --db-url command; a connect-error suggestion names the fix.

Generated output, measured against postgres-meta v0.99.0 on the same schema:

Language Result
Go byte-identical
Swift byte-identical
TypeScript a NOT NULL jsonb column is NonNullable<Json> rather than Json
Python jsonb columns are JsonValue rather than Json[Any]; NotRequired/TypeAlias import from typing_extensions

Both differences are upstream corrections: the old TypeScript type admitted null for a NOT NULL
column, and pydantic's Json[Any] expects an unparsed JSON string rather than a decoded row
value. --linked/--project-id TypeScript is generated server-side by the Management API and is
unaffected, 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 --compile problems with oxfmt — the optional-plugin externals and the statically
embedded napi binding here are that PR's design. Restarted from develop because #6525 moved every
path it touched.

Linked issue

Resolves CLI-2366. Also covers
CLI-2279 (no GitHub issue).

  • The linked issue is open and carries the open-for-contribution label (or I'm a Supabase maintainer).

Checklist

  • The PR title follows Conventional Commits (e.g. fix(cli): …).
  • Tests added or updated for the change.
  • 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

`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>
@Coly010
Coly010 requested a review from a team as a code owner September 16, 2026 17:11
@Coly010 Coly010 self-assigned this Sep 16, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 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.

Comment thread apps/cli/src/commands/gen/types/types.handler.ts Outdated
Comment thread apps/cli/src/commands/gen/types/types.oxfmt.ts Outdated
Comment thread apps/cli/src/commands/gen/types/SIDE_EFFECTS.md Outdated
Comment thread apps/cli/src/commands/gen/types/types.handler.ts
Comment thread apps/cli/tsconfig.types.json Outdated
Comment thread apps/cli/src/commands/gen/types/types.generator.layer.ts Outdated
Comment thread apps/cli/src/command-internal/connect-errors.ts Outdated
Comment thread apps/cli/src/commands/gen/types/types.integration.test.ts Outdated
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 jgoux left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two verified regressions remain after independent review and cross-review.

Comment thread apps/cli/src/commands/gen/types/types.handler.ts Outdated
Comment thread apps/cli/scripts/bundle-externals.ts

@avallete avallete left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment thread apps/cli/src/commands/gen/types/types.handler.ts
Coly010 and others added 2 commits September 17, 2026 10:05
…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>
@Coly010

Coly010 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

All three blockers from @avallete's review are now addressed, plus the earlier AI-review round.

Finding Status Commit
@jgoux — pooler classification dropped the driver cause/code/address fixed cee59507c
@jgouxpnpm cli-release missing the oxfmt externals fixed cee59507c
@avallete--db-url classified local silently ignored explicit sslmode fixed 967878c85

Two of these were real regressions introduced by routing generation through DbConnection, and both were invisible to my own tests — the pooler tests only passed because they fabricated errors that happened to classify true. They now build failures through the real toConnectError, and the TLS coverage asserts whether an SSLRequest actually reaches the wire rather than inspecting config shape.

@avallete — thank you for the dogfooding matrix; that is far better evidence than anything I could produce locally, particularly native-with-Docker-off-PATH, which is the case this change exists for.

One item from the AI round remains open by choice rather than oversight: both bots wanted --network-id to warn-and-ignore instead of failing. I kept the hard error, reasoning on that thread — for a network-only database, ignoring the flag fails the connection anyway but with a worse message, and preserving the capability would mean keeping the whole container path alive for one flag. Happy to revisit if either of you disagrees.

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. --linked TypeScript is served by hosted postgres-meta and so will disagree with local output until that service adopts the same engine.

@Coly010
Coly010 requested a review from avallete September 17, 2026 11:24
@Coly010
Coly010 dismissed avallete’s stale review September 17, 2026 11:24

Stale + Julien re-reviewed

@Coly010
Coly010 added this pull request to the merge queue Sep 17, 2026
Merged via the queue into develop with commit 5ee5ca9 Sep 17, 2026
37 checks passed
@Coly010
Coly010 deleted the columferry/cli-2366-cli-gen-types-local-use-the-selected-managed-stack branch September 17, 2026 11:34

@avallete avallete left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment on lines +404 to +409
export function tlsExplicitlyRequested(cfg: PgConnInput): boolean {
return (
cfg.sslmode !== undefined ||
(cfg.sslrootcert?.length ?? 0) > 0 ||
(cfg.sslrootcertInline?.length ?? 0) > 0
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 || sslrootcertInline

That 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):

  1. ?sslmode=prefer on a loopback DSN. libpq prefer means try TLS, then plaintext. This CLI has never implemented that fallback — prefer is TLS-only — but local classification previously forced plaintext before that mapping ran. After this commit, prefer is “explicit TLS” and the local Postgres that does not speak SSL fails with a handshake error instead of connecting.
  2. PGSSLMODE (or a service-file sslmode) with no sslmode in the DSN. parseConnectionString copies those into cfg.sslmode when the URL omits it (db-config.parse.ts, URL and keyword parsers). A developer with PGSSLMODE=prefer (or require) in the environment, then gen types --db-url postgresql://postgres:postgres@127.0.0.1:54322/postgres, now fails against the local stack. The same path is used by db query --db-url and anything else that goes through acquirePgPoolConnection.

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.

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.

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.

@Coly010

Coly010 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Merged before @avallete's second review landed — the regression it identifies is real and is fixed in #6664.

tlsExplicitlyRequested treated any populated sslmode as a TLS demand. prefer/allow describe a fallback this CLI does not implement, and sslmode is also filled from PGSSLMODE and libpq service files, so an ambient PGSSLMODE=prefer broke local --db-url across db query, dump and pull as well as gen types. #6664 narrows the predicate to require/verify-ca/verify-full or a supplied root cert, with a before/after matrix against a plaintext local Postgres.

pull Bot pushed a commit to chizee/cli that referenced this pull request Sep 18, 2026
…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>
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.

3 participants