Check the project first. The report lists, with file and line, what to change before switching, what to review and what is worth knowing; it changes nothing:
npx @steadybrew/postgresjs-migrate .Then switch. This points every postgres dependency at the fork through an npm alias, adds overrides for dependencies that bring their own copy, and removes a postgres@3.4.9 patch the fork already contains. It doesn't touch your code or lockfile, so run your package manager's install afterwards:
npx @steadybrew/postgresjs-migrate . --switchThe same check and switch run from the Codemod registry with npx codemod @steadybrew/postgresjs-migrate. The tool's README has the full list of findings. To switch by hand instead:
npm install postgres@npm:@steadybrew/postgresjs@^4Either way the fork is installed under the name postgres, so import postgres from 'postgres' stays as it is, and so do libraries that load postgres by name, such as drizzle-orm/postgres-js, drizzle-kit and kysely-postgres-js.
From 4.5.0, dd-trace records the same database spans as with Postgres.js when the fork is installed under the postgres name. Sentry's automatic integration instruments postgres versions below 4 only, so widen its range before Sentry loads:
// sentry-postgres.mjs
import { SENTRY_RUNTIME_INSTRUMENTATIONS } from '@sentry/server-utils/orchestrion/config'
for (const instrumentation of SENTRY_RUNTIME_INSTRUMENTATIONS) {
if (instrumentation.module.name === 'postgres')
instrumentation.module.versionRange = '>=3.0.0 <5'
}node --import ./sentry-postgres.mjs --import @sentry/node/import app.jsThis covers Sentry's runtime hook, not its bundler plugins. With pnpm, add @sentry/server-utils at the same version as @sentry/node so the file can import it.
What changes for code written against Postgres.js 3.4 or @steadybrew/postgresjs 3.x. Version 4.0.0 holds the breaking changes; the later items are behaviour changes in 4.x releases.
- Node.js compatibility. Tested on Node.js 22, 24 and 26. Older versions may work but aren't tested; the package declares
engines.node: >=22, so package managers may warn or reject installation on older versions. With TypeScript, the declarations need@types/node22 or newer, orskipLibCheck. - Handles stay with their owner. A reserved handle used after
release(), and a transaction handle used after its transaction finished or while itsCOMMITis in flight, rejects withCONNECTION_RELEASEDfor a reservation andTRANSACTION_ENDEDfor a transaction. 4.0 to 4.6 usedCONNECTION_ENDEDfor both, so if you retry onCONNECTION_ENDED, these now fall outside that set, which is right: they are bugs to fix, not to retry.CONNECTION_ENDEDnow means only thatsql.end()was called. Since 4.7.0. A handle whose connection closed rejects withCONNECTION_CLOSEDinstead of running on a reconnected session, until its reservation is released or its transaction callback has finished; 4.0 to 4.6 keptCONNECTION_CLOSEDafter that, and 4.7.0 gives the codes above. sql.prepare(name)in a transaction takes a non-empty string without NUL characters; other names throwINVALID_TRANSACTION_NAME.fetch_types: falsenow decodes built-in arrays as JavaScript arrays instead of raw strings. SQLNULLelements becomenull; quoted"NULL"text remains a string.- Callbacks that throw (
onnotice,onparameter,onnotify,onclose,onidle,debug,backoff, and thesql.listen()andsql.subscribe()callbacks) surface as uncaught exceptions, which end the process under Node's default handling. Since 4.4.0. COPYkeeps its connection; other queries use another connection or wait. Since 4.4.1, when every connection is held by aCOPY, queries and reservations reject withCOPY_IN_PROGRESSinstead of hanging. Finish writing or reading your ownCOPYbefore awaiting another query withmax: 1. Queries on a reserved or transaction handle still reject duringCOPY.- Environment values
falseanddisableturn options off, soPGSSL=disablemeans no TLS, where 3.4.9 read it as "require TLS". The same goes forPGPREPARE,PGFETCH_TYPESandPGDEBUG. Since 4.5.0. target_session_attrs: 'read-write'no longer connects to a hot standby, and'read-only'accepts one, as in libpq. A mismatch rejects withCONNECTION_CLOSEDwhosecauseis aTARGET_SESSION_ATTRSerror naming each host. Since 4.1.0 and 4.5.0.- A transaction no longer commits half its work when a query in it fails or is cancelled before it is sent, such as one with an
undefinedvalue or onequery.cancel()reached while it was still queued. Before 4.7.0, a transaction you run yourself on a reserved connection or withmax: 1could catch that query's error and commit the other statements without it, so a transfer could commit e.g. the debit without the credit. Now the whole transaction rolls back, and the data stays as it was before the transaction started. Upgrading does not repair data written before then: if your code caught such errors, those transactions may have left half-written data, such as a debit with no credit. Since 4.7.0. - Idle connections past
idle_timeoutormax_lifetimeare closed when handed out instead of being used, and a graceful close is bounded byconnect_timeout. Since 4.1.0.
The changelog has every change, release by release.