Native PostgreSQL driver
Status: frozen for implementation on 2026-09-27. Remove this specification when every Effect package resolves to one version, the @effect/sql-pg exception is gone from the version check, and AGENTS.md and docs/architecture.md describe the pools.
Goal
Move packages/database from @effect/sql-pg 4.0.0-rc.112, which wraps the pg library through PgClient.fromPool, to the native wire client, so every Effect package resolves to the same release (operator rule, 2026-09-27). Until this lands, rc.112 is the single named exception in the Effect version check.
Evidence (research of 2026-09-27; re-verify before building)
- rc.113 (Effect PR #7426) removed
fromPool,fromClient, andmakeWith, and dropped thepgdependency. It also:- changes
typestoPgTypes.Registry; - stops treating object parameters as JSON;
- rejects multi-statement query strings;
- changes the result codecs (
int8becomesbigint,datea string, timestampsDate,byteaUint8Array, unknown OIDs text); - enables named prepared statements by default (
PgClientConfig.prepare); - turns
listeninto a scoped dequeue. rc.114 sets the TLS SNI hostname by default for non-IP hosts. See the tagged CHANGELOG andPgTypes.ts:1240-1320ateffect@4.0.0-rc.116.
- changes
- Better Auth and the Effect
Databaseshare onepg.Poolobject, but they never share a transaction or a connection.pgTransactionis a client-scopedBEGIN/COMMIT; Better Auth's adapter calls and its hooks run outside any Effect transaction; transactional native commands read Better Auth's tables through Effect SQL (authority.ts:121-160,270-355). This comes from a code-path audit, not runtime tracing. - No Effect v4
@effect/sql-kyselyexists: 0.48.0 peers on Effect 3. Better Auth's PostgreSQL support is Kysely over apg.PoolorPostgresDialect. - Codec census amendment, 2026-10-09: the earlier claim of no uncast
int8reads was incomplete. The stable native client decodesint8to JavaScriptbigint. The source audit found two breaking decoder sites: the news department-linkarticle_idand the legacy receipt rehearsalfile_byte_length. Both now select text and decode through their domain-bounded numeric schemas. Range-unsafe int4 casts on article ids and receipt byte lengths were also removed. Timestamp, date, time and array compatibility is checked by the native codec/PGlite parity tests; real PostgreSQL17/18 acceptance remains required. Migration source statement boundaries use the maintained parser and the caller-owned native transaction, not multi-statementsql.unsafe. - The native client supports
prepare: false. PgBouncer 1.21+ supports named prepared statements in transaction mode only whenmax_prepared_statementsis nonzero.
Decision
Two pools.
- The Effect
Databaseuses the native client throughPgClient.layer, owning its pool, with at most 8 connections (today's cap). - Better Auth keeps its own
pg.Poolthrough its supported Kysely/pg adapter, with at most 4 connections. - That is 12 connections per backend process instead of 8. The deployment's connection budget must allow 12 × replicas, plus workers and CLIs.
A Kysely dialect over the native client and a custom Better Auth adapter are both rejected: one is unverified on v4, and the other reimplements adapter semantics.
Rules
- Each pool is a scoped Layer that closes only its own pool. No module outside
packages/databasetouches apg.Pool, andpgQuery/pgTransaction/pgWithClientexist only for Better Auth's hooks, or are removed. - Migration execution splits each migration file into statements with a parser that respects dollar quoting, comments, and string literals, and runs them in one transaction; or it uses the native client's documented multi-statement path if one exists.
just migration-hashesand the upgrade proofs stay green. - Named prepared statements are disabled (
prepare: false) unless the deployment's PgBouncer setsmax_prepared_statements. The choice is one configuration key, tested both ways. - Search-path amendment, 2026-10-09: every native and Better Auth pool requires caller-supplied
startupOptions; neitherDatabaseLive,DatabaseRuntimeLive,nativePgLayernorAuthPoolLivesupplies asearch_pathfallback. Production native database hosts declaresearch_path=public; supported Better Auth hosts declaresearch_path=auth. Historical schema-move proofs may explicitly declareauth,publicto reproduce their earlier layout. Other startup settings (such as TimeZone) stay explicit in the same options. The shadowing regression builds both native database layers with the declared policy and proves an unqualified production table resolves to public even when auth contains a same-named table; the SQL lead also inventories auth/public catalog collisions. TLS and schema ownership remain unchanged. - Codecs are declared, not left to defaults (sql-pg CHANGELOG rc.113 #7426, rc.116 #8240 and #8241):
- Every PostgreSQL enum or domain type that a query reads is registered through
PgTypes.register, or cast to text in SQL. Unregistered OIDs now decode as UTF-8 text, and binary UDTs or enum arrays can garble a value or close the connection. Dateparameters now bind astimestamptz. Every write to atimestamp without time zonecolumn is listed and either casts explicitly or pins the sessionTimeZone, so a write can't shift with the server's zone.±infinitytimestamps decode as an invalidDate. Find where they can occur and reject them.
- Every PostgreSQL enum or domain type that a query reads is registered through
multiplexConcurrency(default 32) andconnectionTTLare set explicitly, with their reasons, beside the pool size.
Done when
bun.lockresolves@effect/sql-pgto the same version aseffect, and the version check has no exception.- Pool ownership on real PostgreSQL: each pool honours its limit; shutting down with an active query or callback leaks no client; closing one pool doesn't affect the other.
- Sign-in, password recovery, a cookie-authenticated native command, transactional session revocation, and a rolled-back command behave as today, shown by the existing identity and recovery suites plus the identity-postgres proof.
- A fresh database and the migration upgrade proofs pass with the new migration runner. A migration with a dollar-quoted function body runs correctly (test).
delivery-pgbouncer.test.ts, plus a native-client transaction and a repeated query through PgBouncer in transaction mode, pass withprepare: false, and withprepare: trueagainstmax_prepared_statements > 0.- The golden journeys and
just checkpass, and the hosted workflows are green.
Order
After the review fixes that touch packages/database land. They change pg-pool.ts callers: OAuthFix, OrganizationFix, AdmissionsCoreFix, EconomyFix, DeliveryFix, ContentTeamsFix.
Stable release and local Worker amendment (2026-10-09)
The approved target is Effect and SQL 4.0.0. No release-candidate adapter remains. The two-pool decision stays in force. No custom Better Auth adapter or Kysely driver replaces the supported adapter.
Each host supplies separate native and auth connection limits:
| Owner | Native Database | Better Auth | Total |
|---|---|---|---|
| Bun backend process | 8 | 4 | 12 |
| Local Worker fetch | 2 | 2 | 4 |
| Local Worker delivery event | 2 | none | 2 |
An overlapping fetch and its delivery event can use six application connections. Concurrent invocations add their limits. This local amendment grants no cloud configuration or deployment authority.
The request signal and response body own the fetch graph. Each tail or Cron event owns a separate graph. No event captures the request pool, runtime, memo map, or signal. A delivery-only graph acquires no auth pool.
Worker acquisition retains its existing absolute deadlines and server preflight.
The server must enforce statement_timeout of at most 1s and idle_in_transaction_session_timeout of at most 2s.
The Worker runs no migrations, session SET, or startup GUC commands.
Transaction-local commands remain inside the native transaction. Isolation configuration precedes every other transaction statement.
The old RC pool evidence does not prove stable-driver cleanup. The existing bounded-cleanup acceptance remains a requirement. A real saturated-pool test must observe interrupted transaction acquisition with no late query or write. A real transport test must observe physical socket closure, separately from scope completion and the remote transaction outcome. If the native driver fails either requirement, a minimal native source repair needs approval and a real afterproof. No timeout fallback, private transaction service, fabricated close receipt, or weaker deadline replaces that requirement.
The native driver accepts one extended-protocol statement, including with prepare: false.
The migration boundary uses the maintained PostgreSQL parser and original UTF-8 source offsets.
Its tests cover dollar quoting, nested comments, string literals, Unicode, and rollback after a later statement fails.
The migration ledger and applied migration checksums remain unchanged.
Native JSON binds the value once. Empty arrays and affected-row predicates retain their existing meaning.
Domain callers use native Database transactions. Only the supported Better Auth boundary retains raw pg access.
Provider work starts after the claim transaction releases its connection. Settlement uses a new fenced transaction.