Library / SDK
porsager/postgres avatar
porsager/postgres

Postgres.js v4 hands parameter typing to the server, and that changes how your Dates land

Postgres.js - The Fastest full featured PostgreSQL client for Node.js, Deno, Bun and CloudFlare

8,767 stars409 forksJavaScriptUnlicense

At a glance

What is it?
Postgres.js is a dependency free PostgreSQL client for Node.js, Bun and Cloudflare Workers built around tagged template queries. Version 4.0.0, released on 2026-10-29, keeps the v3 API but lets the server decide parameter types, which is the first thing an upgrading team has to check.
Who is it for?
Postgres.js fits JavaScript teams on Node.js 20 or later, Bun or Cloudflare Workers who want SQL written as SQL, with parameters bound by the protocol and no dependency tree. Teams upgrading from v3 should hold off until they have checked every Date written to timestamp or date columns outside UTC and every untyped parameter in a select list, or they should turn on infer: true.
Can I use it commercially?
Yes. Unlicense is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • porsager/postgres8,767 ★ · +37
Star history
Repository2026-09-222026-11-13
porsager/postgres8.7k8.8k

37 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Every query is a tagged template that becomes $1, $2 placeholders

The whole client hangs off one function. You create an instance once and import it wherever you query:

js
// db.js
import postgres from 'postgres'

const sql = postgres({ /* options */ }) // will use psql environment variables

export default sql

A query is then a tagged template such as sql`select name, age from users where age > ${ age }`. Postgres.js processes the interpolated values before they reach the SQL string: each one is replaced by a protocol placeholder ($1, $2 and so on) and sent to the server separately, and the server handles escaping and casting. That is why the project can say SQL injection through parameters is not possible without any special handling. Every query resolves to a Result array of objects that map column names to row values, so an insert with returning * gives back rows such as [{ user_id: 1, name: 'Murray', age: 68 }].

Quoting an interpolated value breaks the query

The placeholder mechanism has one trap that catches people coming from string built SQL. Because the template replaces ${name} with $1, writing '${name}' inside quotes turns the parameter into the literal string '$1', and Postgres reads it as text instead of a parameter, which causes an error. The fix is to drop the quotes and let Postgres infer the column type. The same rule applies to patterns: build the pattern in JavaScript, as in ${ name + '%' } for a like clause, rather than gluing a % onto a quoted placeholder. A second behavior that surprises new users is that a query does nothing until it is awaited. Building a sql`` template without await, or without calling .execute() to run it at once, sends nothing to the database, so a forgotten await is a silent no-op rather than a fire and forget write.

In v4 the server, not JavaScript, picks each parameter type

Version 4.0.0 is the release that matters most for existing users. The API stays the same apart from a short list of changes, but parameter types are now decided by the server from the query, the way libpq and psql do it, and each value is written for the type the server asked for. A Date bound to a timestamptz or timestamp column is stored as the instant it is, a timestamp is read back as UTC, and a string such as 'true' sent for a boolean column is parsed by the server. The consequence for anyone running outside UTC is spelled out by the project itself: how a Date is written to timestamp and date columns changes. When the query does not fix a type, for example a value in the select list or an argument to an overloaded function like date_trunc, the server reads the value as text or asks for a cast. A cast settles it:

js
await sql`select date_trunc('day', ${ date }::timestamptz)`
await sql`select ${ flag }::bool as flag`

infer: true restores the v3 typing rules

Teams that cannot audit every query before upgrading have an escape hatch. Setting infer: true types values from JavaScript the way v3 did: a Date becomes timestamptz, a Buffer or Uint8Array becomes bytea, a boolean becomes boolean, a bigint becomes int8, and an array is typed by its first element. That last rule matters for mixed or empty arrays, since only the first element decides. The tradeoff is that you keep the old client side guessing instead of the server's answer, so the v4 behavior for timestamp columns does not apply. The project points to its Upgrading from v3 section for the full list of what changes, and anyone moving from v3.4.9 (2026-04-05) or v3.4.8 (2026-01-06) to v4.0.0 should read it before bumping the version, since the date handling change is about which values get written, not about which calls are allowed.

The README asks for Node.js 20 while package.json allows 18

Postgres.js is an ES module with no dependencies. Installation is a single package:

bash
$ npm install postgres

The stated runtime requirement is Node.js 20 or later, Bun, or Cloudflare Workers, with Deno running it through its Node.js compatibility layer. The engines field in package.json, however, still reads node >=18, so npm will not stop an install on Node.js 18 even though that version sits below the stated requirement. If your CI or production image runs Node.js 18, nothing in the install step will warn you. CommonJS has its own threshold. A dynamic import works everywhere:

js
const { default: postgres } = await import('postgres')

Plain require only works on Node.js 20.19 or 22.12 and later:

js
const postgres = require('postgres').default

Both forms read the client from .default, so code ported from another driver that expects the function as the module itself needs that one change.

A workerd export condition swaps in a separate Cloudflare build

The package exports map has three entries for the root import. TypeScript resolves types from ./types/index.d.ts, the workerd condition used by Cloudflare Workers loads ./src/cf.js, and every other runtime gets ./src/index.js. Only /src/*.js and /types/index.d.ts are published, so the tests and tooling stay out of node_modules. The Cloudflare path has its own test script, test:cf, which runs sh tests/cf/run.sh, separate from the default test script that chains unit tests, the integration suite in tests/index.js and tests/simulate.js. Type definitions are checked by test:types, which installs and tests inside the types folder. Publishing is gated by prepublishOnly, which runs eslint with --max-warnings 0 over src and tests and then the full test script.

Connection settings stack from URL, options and psql variables

postgres([url], [options]) accepts a postgres:// connection string, an options object, or both. Values in the object override anything present in the URL, and anything left unset falls back to the same environment variables psql reads. Calling postgres() with an empty options object therefore connects using whatever PG variables the process inherited, which is convenient locally and a source of surprises in containers where those variables point at another database. The visible options are host and port, both of which accept multiple values according to their comments, plus database, username and password, with port defaulting to 5432; the rest sits in the Connection details section. Beyond basic queries, the documentation has sections on transactions, listen and notify, realtime subscribe, custom types, teardown, tracing, error handling and reserving connections.

sql(object) inserts every field unless you name the columns

Dynamic queries reuse the sql function as a helper inside the template. sql(columns) with an array produces a quoted column list, so ['name', 'age'] becomes select "name", "age" from users. For inserts, sql(user, 'name', 'age') or sql(user, columns) yields insert into users ("name", "age") values ($1, $2). The shortcut sql(user) with no column list takes every field on the object as a column. That is where the helper stops protecting you: if the object comes straight from a request body, a client can add fields you never meant to write, such as a role or an owner id. The project warns against letting users supply columns you do not want inserted. Pass an explicit column list for any object that crosses a trust boundary.

Editorial conclusion

Postgres.js fits JavaScript teams on Node.js 20 or later, Bun or Cloudflare Workers who want SQL written as SQL, with parameters bound by the protocol and no dependency tree. Teams upgrading from v3 should hold off until they have checked every Date written to timestamp or date columns outside UTC and every untyped parameter in a select list, or they should turn on infer: true. CommonJS projects on Node.js older than 20.19 or 22.12 need the dynamic import path.

Frequently asked questions

What is Postgres.js?

Postgres.js is a PostgreSQL client for Node.js, Bun and Cloudflare Workers, with Deno supported through its Node.js compatibility. Queries are written as ES6 tagged template strings, and parameters are sent to the server separately as $1, $2 placeholders.

What is Postgres.js written in?

Postgres.js is written in JavaScript and ships as an ES module with no dependencies. TypeScript definitions are published in types/index.d.ts.

Is Postgres.js open source?

Yes. Postgres.js is released under the Unlicense, and the source is on GitHub at porsager/postgres.

Is Postgres.js the same thing as PostgreSQL?

No. PostgreSQL is the database server; Postgres.js is a JavaScript client that connects to it with a postgres:// URL or options such as host, port, database, username and password.

Official sources

  1. Issues
  2. License: Unlicense
  3. porsager/postgres on GitHub
  4. README
  5. Releases
Community

Where developers are discussing it

Posts on Hacker News, dev.to and Lobsters that link to this repository, found by our scan of those communities. 1 in the last 30 days; newest first.

Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/porsager-postgres.svg)](https://hysenlabs.com/projects/porsager-postgres)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/porsager-postgres.svg)](https://hysenlabs.com/projects/porsager-postgres)

Was this useful?