Profile
Back to NewsBack
GitHub Trending 34 min
Reader Mode
porsager/postgres: Postgres.js - The Fastest full featured PostgreSQL client for Node.js, Deno, Bun and CloudFlare

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

13 hours ago
Fastest full PostgreSQL nodejs client
  • 🚀 Fastest full-featured PostgreSQL client
  • 🏷 ES6 Tagged Template Strings at the core
  • 🏄‍♀️ Simple surface API
  • 🖊️ Dynamic query support
  • 🐦 Follow on X

Getting started


Good UX with Postgres.js

Installation

$ npm install postgres

Postgres.js needs Node.js 20 or later, Bun, or Cloudflare Workers. Deno runs it through its Node.js compatibility. It is an ES module with no dependencies.

Upgrading from v3? The API is the same apart from a short list of changes. Read Upgrading from v3 first: parameter types are now decided by the server, which changes how a Date is written to timestamp and date columns outside UTC.

Usage

Create your sql database instance
// db.js
import postgres from 'postgres'

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

export default sql

Simply import for use elsewhere

// users.js
import sql from './db.js'

async function getUsersOver(age) { const users = await sql select name, age from users where age > ${ age } // users = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] return users }

async function insertUser({ name, age }) { const users = await sql insert into users (name, age) values (${ name }, ${ age }) returning name, age // users = Result [{ name: "Murray", age: 68 }] return users }

Dynamic imports and CommonJS

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

From CommonJS, on Node.js 20.19 or 22.12 and later, require loads it as well:

const postgres = require('postgres').default

Table of Contents

Connection

postgres([url], [options])

You can use either a postgres:// url connection string or the options to define your database connection properties. Options in the object will override any present in the url. Options will fall back to the same environment variables as psql.

const sql = postgres('postgres://username:password@host:port/database', {
  host                 : '',            // Postgres ip address[s] or domain name[s]
  port                 : 5432,          // Postgres server port[s]
  database             : '',            // Name of database to connect to
  username             : '',            // Username of database user
  password             : '',            // Password of database user
  ...and more
})

More options can be found in the Connection details section.

Queries

``await sql... -> Result[]
Postgres.js utilizes Tagged template functions to process query parameters before interpolation. Using tagged template literals benefits developers by:

  1. Enforcing safe query generation
  2. Giving the sql` function powerful utility and query building features.
Any generic value will be serialized according to an inferred type, and replaced by a PostgreSQL protocol placeholder $1, $2, .... The parameters are then sent separately to the database which handles escaping & casting.

All queries will return a Result array, with objects mapping column names to each row.js const xs = await sql insert into users ( name, age ) values ( 'Murray', 68 )

returning *

// xs = [{ user_id: 1, name: 'Murray', age: 68 }]

> Please note that queries are first executed when awaited – or instantly by using .execute().

Query parameters

Parameters are automatically extracted and handled by the database so that SQL injection isn't possible. No special handling is necessary, simply use tagged template literals as usual.

js const name = 'Mur' , age = 60

const users = await sql select name, age from users where name like ${ name + '%' } and age > ${ age } // users = [{ name: 'Murray', age: 68 }]

> Be careful with quotation marks here. Because Postgres infers column types, you do not need to wrap your interpolated parameters in quotes like '${name}'. This will cause an error because the tagged template replaces ${name} with $1 in the query string, leaving Postgres to do the interpolation. If you wrap that in a string, Postgres will see '$1' and interpret it as a string as opposed to a parameter.

Parameter types

The server decides the type of every parameter from the query, as with libpq and psql, and each value is written for the type the server asked for. A Date for 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' for a boolean is read by the server.

Where the query doesn't decide the type, such as 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 always decides:

js await sqlselect date_trunc('day', ${ date }::timestamptz) await sqlselect ${ flag }::bool as flag
infer: true types values from JavaScript instead, as v3 did: a Date as timestamptz, a Buffer or Uint8Array as bytea, a boolean as boolean, a bigint as int8, and an array by its first element. See Upgrading from v3 for what that changes.

Dynamic column selection

js const columns = ['name', 'age']

await sql select ${ sql(columns) } from users

// Which results in: select "name", "age" from users

### Dynamic inserts
js const user = { name: 'Murray', age: 68 }

await sql insert into users ${ sql(user, 'name', 'age') }

// Which results in: insert into users ("name", "age") values ($1, $2)

// The columns can also be given with an array const columns = ['name', 'age']

await sql insert into users ${ sql(user, columns) }

You can omit column names and simply execute sql(user) to get all the fields from the object as columns. Be careful not to allow users to supply columns that you do not want to be inserted.

Multiple inserts in one query

If you need to insert multiple rows at the same time it's also much faster to do it with a single insert. Simply pass an array of objects to sql().
js const users = [{ name: 'Murray', age: 68, garbage: 'ignore' }, { name: 'Walter', age: 80 }]

await sqlinsert into users ${ sql(users, 'name', 'age') }

// Is translated to: insert into users ("name", "age") values ($1, $2), ($3, $4)

// Here you can also omit column names which will use object keys as columns await sqlinsert into users ${ sql(users) }

// Which results in: insert into users ("name", "age") values ($1, $2), ($3, $4)

### Dynamic columns in updates
This is also useful for update queries
js const user = { id: 1, name: 'Murray', age: 68 }

await sql update users set ${ sql(user, 'name', 'age') } where user_id = ${ user.id }

// Which results in: update users set "name" = $1, "age" = $2 where user_id = $3

// The columns can also be given with an array const columns = ['name', 'age']

await sql update users set ${ sql(user, columns) } where user_id = ${ user.id }

### Multiple updates in one query
To create multiple updates in a single query, it is necessary to use arrays instead of objects to ensure that the order of the items correspond with the column names.
js const users = [ [1, 'John', 34], [2, 'Jane', 27], ]

await sql update users set name = update_data.name, age = (update_data.age)::int from (values ${sql(users)}) as update_data (id, name, age) where users.id = (update_data.id)::int returning users.id, users.name, users.age

### Dynamic values and where in
Value lists can also be created dynamically, making where in queries simple too.
js const users = await sql select * from users where age in ${ sql([68, 75, 23]) }
or
js const [{ a, b, c }] = await sql select * from (values ${ sql(['a', 'b', 'c']) }) as x(a, b, c)
### Named arguments in function calls
An object in the parentheses of a function call becomes its named arguments.
js await sql select make_interval(${ sql({ days: 1, hours: 2 }) })

// Which results in: select make_interval("days" => $1,"hours" => $2)

## Building queries

Postgres.js features a simple dynamic query builder by conditionally appending/omitting query fragments. It works by nesting sql fragments within other sql calls or fragments. This allows you to build dynamic queries safely without risking sql injections through usual string concatenation.

Partial queries

js const olderThan = x => sqland age > ${ x }

const filterAge = true

await sql select * from users where name is not null ${ filterAge ? olderThan(50) : sql } // Which results in: select * from users where name is not null // Or select * from users where name is not null and age > 50

### Dynamic filters
js await sql select * from users ${ id ? sqlwhere user_id = ${ id } : sql }

// Which results in: select * from users // Or select * from users where user_id = $1

Identifiers and values combine with operators, such as for a field of a json column:
js const column = email ? 'notifications' : 'settings'

await sql select * from users where ${ sql(column) }->>'email' = ${ 'true' }

// Which results in: select * from users where "notifications"->>'email' = $1

### Joining fragments

A fragment used as a separator joins a list of fragments and values. Values become parameters, and false, null and undefined are left out, so conditions can be written inline. A space is added on each side of the separator, and around the result, where there isn't one already.

js const where = sqland.join([ sqlage > ${ age }, name && sqlname = ${ name } ])

await sql select * from users ${ where.length ? sqlwhere ${ where } : sql }

// Which results in: select * from users where age > $1 and name = $2

### Dynamic ordering
js const id = 1 const order = { username: 'asc', created_at: 'desc' } await sql select * from ticket where account = ${ id } order by ${ Object.entries(order).flatMap(([column, order], i) => [i ? sql, : sql, sql${ sql(column) } ${ order === 'desc' ? sqldesc : sqlasc }] ) }
### SQL functions
Using keywords or calling functions dynamically is also possible by using
sql` ` fragments.
const date = null

await sql update users set updated_at = ${ date || sqlnow() }

// Which results in: update users set updated_at = now()

Table names

Dynamic identifiers like table names and column names is also supported like so:
const table = 'users'
    , column = 'id'

await sql select ${ sql(column) } from ${ sql(table) }

// Which results in: select "id" from "users"

Quick primer on interpolation

Here's a quick oversight over all the ways to do interpolation in a query template string:

| Interpolation syntax | Usage | Example | | ------------- | ------------- | ------------- | | ${ sql` } | for keywords or sql fragments | `await sqlSELECT * FROM users ${sqlorder by age desc } | | ${ sql(string) } | for identifiers | await sqlSELECT * FROM ${sql('table_name') | | ${ sql([] or {}, ...) } | for helpers | await sqlINSERT INTO users ${sql({ name: 'Peter'})} | | ${ 'somevalue' } | for values | await sqlSELECT * FROM users WHERE age = ${42} |

Advanced query methods

Cursors

`await sql.cursor([rows = 1], [fn])
Use cursors if you need to throttle the amount of rows being returned from a query. You can use a cursor either as an async iterable or with a callback function. For a callback function new results won't be requested until the promise / async callback function has resolved.

Inside sql.begin or on a reserved connection every cursor has a portal of its own, so several can be open at once and other queries can run between their batches.

##### callback functionjs await sql select * from generate_series(1,4) as x .cursor(async([row]) => { // row = { x: 1 } await http.request('https://example.com/wat', { row }) })

##### for await...of
js // for await...of const cursor = sqlselect * from generate_series(1,4) as x.cursor()

for await (const [row] of cursor) { // row = { x: 1 } await http.request('https://example.com/wat', { row }) }

A single row will be returned by default, but you can also request batches by setting the number of rows desired in each batch as the first argument to .cursor:
js await sql
select * from generate_series(1,1000) as x .cursor(10, async rows => { // rows = [{ x: 1 }, { x: 2 }, ... ] await Promise.all(rows.map(row => http.request('https://example.com/wat', { row }) )) })
If an error is thrown inside the callback function no more rows will be requested and the outer promise will reject with the thrown error.

You can close the cursor early either by calling break in the for await...of loop, or by returning the token sql.CLOSE from the callback function.

js await sql
select * from generate_series(1,1000) as x .cursor(row => { return Math.random() > 0.9 && sql.CLOSE // or sql.END })
### Instant iteration

####

await sql
.forEach(fn)
If you want to handle rows returned by a query one by one, you can use .forEach which returns a promise that resolves once there are no more rows.
js await sql
select created_at, name from events .forEach(row => { // row = { created_at: '2019-11-22T14:22:00Z', name: 'connected' } })

// No more rows

### Query Descriptions
####
await sql
.describe() -> Result[]
Rather than executing a given query, .describe will return information utilized in the query process. This information can include the query identifier, column types, etc.

This is useful for debugging and analyzing your Postgres queries. Furthermore, .describe will give you access to the final generated query string that would be executed.

Rows as Array of Values

####
sql
.values()
Using .values will return rows as an array of values for each column, instead of objects.

This can be useful to receive identically named columns, or for specific performance/transformation reasons. The column definitions are still included on the result array, plus access to parsers for each column.

Rows as Raw Array of Buffers

####
sql
.raw()
Using .raw will return rows as an array with Buffer values for each column, instead of objects.

This can be useful for specific performance/transformation reasons. The column definitions are still included on the result array, plus access to parsers for each column.

Queries in Files

await sql.file(path, [args], [options]) -> Result[]

Using a file for a query is also supported with optional parameters to use if the file includes $1, $2, etc

js const result = await sql.file('query.sql', ['Murray', 68])
### Multiple statements in one query
####
await sql
.simple()
The postgres wire protocol supports "simple" and "extended" queries. "simple" queries supports multiple statements, but does not support any dynamic parameters. "extended" queries support parameters but only one statement. To use "simple" queries you can use
sql
.simple()`. That will create it as a simple query.

await sqlselect 1; select 2;.simple()

Copy to/from as Streams

Postgres.js supports COPY ... queries, which are exposed as Node.js streams.

`await sqlcopy ... from stdin.writable() -> Writable
js

import { pipeline } from 'node:stream/promises'

// Stream of users with the default tab delimitated cells and new-line delimitated rows const userStream = Readable.from([ 'Murray\t68\n', 'Walter\t80\n' ])

const query = await sqlcopy users (name, age) from stdin.writable() await pipeline(userStream, query); query.count // 2, the number of rows copied, once the stream has finished

####
await sqlcopy ... to stdout.readable() -> Readable
##### Using Stream Pipeline
js import { pipeline } from 'node:stream/promises' import { createWriteStream } from 'node:fs'

const readableStream = await sqlcopy users (name, age) to stdout.readable() await pipeline(readableStream, createWriteStream('output.tsv')) // output.tsv content: Murray\t68\nWalter\t80\n

##### Using for await...of
js const readableStream = await sql copy ( select name, age from users where age = 68 ) to stdout .readable() for await (const chunk of readableStream) { // chunk.toString() === Murray\t68\n }
> NOTE This is a low-level API which does not provide any type safety. To make this work, you must match your copy query parameters correctly to your Node.js stream read or write code. Ensure Node.js stream backpressure is handled correctly to avoid memory exhaustion.

Canceling Queries in Progress

Postgres.js supports, canceling queries in progress. It works by opening a new connection with a protocol level startup message to cancel the current query running on a specific connection. That means there is no guarantee that the query will be canceled, and due to the possible race conditions it might even result in canceling another query. This is fine for long running queries, but in the case of high load and fast queries it might be better to simply ignore results instead of canceling.

js const query = sql
select pg_sleep 100.execute() setTimeout(() => query.cancel(), 100) const result = await query
.signal() cancels the query when an AbortSignal aborts: one still waiting for a connection is rejected without being sent, and a running one gets a cancel request. A cursor canceled while its callback runs, or while an async iterator over it is between batches, is stopped by the client instead: it fetches no more rows, closes its portal and rejects with 57014, with no cancel request sent.
js await sql
select * from report().signal(AbortSignal.timeout(5000))
A query waits for a connection when every connection is reserved, in a transaction or has max_pipeline queries in flight. queue_timeout (in seconds) rejects it with QUEUE_TIMEOUT if it waits longer, without ever sending it, and the same goes for sql.begin and sql.reserve. Once a query is sent, PostgreSQL's own statement_timeout is the limit.

Execute

####

await sql
.execute()
The lazy Promise implementation in Postgres.js is what allows it to distinguish Nested Fragments from the main outer query. This also means that queries are always executed at the earliest in the following tick. If you have a specific need to execute the query in the same tick, you can call .execute()

Inspecting a query

query.text is the text sent to the server, with $1, $2 for the parameters, and query.parameters their values. Both work before the query is sent, without a connection:

js const query = sql
select * from users where id = ${ 1 } and name = ${ 'Murray' } query.text // 'select * from users where id = $1 and name = $2' query.parameters // [1, 'Murray']
### Unsafe raw string queries

<details> <summary>Advanced unsafe use cases</summary>

await sql.unsafe(query, [args], [options]) -> Result[]

If you know what you're doing, you can use unsafe to pass any string you'd like to postgres. Please note that this can lead to SQL injection if you're not careful.

js sql.unsafe('select ' + danger + ' from users where id = ' + dragons)
Values go in args and are referred to as $1, $2 and so on, so they are never part of the string itself:
js sql.unsafe('select * from users where id = $1 and age > $2', [id, age])
options takes:

| Option | Default | | | --- | --- | --- | | prepare | false | Prepare the query as a named statement. Off by default because a string built at run time may be different every time, and each would stay prepared on the server. | | simple | true without args | Send it with the simple protocol, which allows several statements in one string but no args. |

You can also nest sql.unsafe within a safe sql expression. This is useful if only part of your fraction has unsafe elements.

js const triggerName = 'friend_created' const triggerFnName = 'on_friend_created' const eventType = 'insert' const schema_name = 'app' const table_name = 'friends'

await sql create or replace trigger ${sql(triggerName)} after ${sql.unsafe(eventType)} on ${sql.unsafe(${schema_name}.${table_name})} for each row execute function ${sql(triggerFnName)}()

await sql create role friend_service with login password ${sql.unsafe('${password}')}

</details>

Transactions

BEGIN / COMMIT await sql.begin([options = ''], fn) -> fn()

Use sql.begin to start a new transaction. Postgres.js will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.begin will resolve with the returned value from the callback function.

BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.

A query that fails in a transaction aborts it, whether the server rejected it or Postgres.js could not build it (an undefined value, say), as PostgreSQL does: catching the error does not let the rest commit, and a later COMMIT answers ROLLBACK. The transaction commits only once its cursors have finished, also those the callback did not await.

js const [user, account] = await sql.begin(async sql => { const [user] = await sql insert into users ( name ) values ( 'Murray' ) returning *

const [account] = await sql insert into accounts ( user_id ) values ( ${ user.user_id } ) returning *

return [user, account] })

Do note that you can often achieve the same result using WITH queries (Common Table Expressions) instead of using transactions.

It's also possible to pipeline the requests in a transaction if needed by returning an array with queries from the callback function like this:

js const result = await sql.begin(sql => [ sql
update ..., sqlupdate ..., sqlinsert ... ])
Queries that are not awaited one by one are pipelined anywhere in the transaction, so Promise.all pipelines a group of them between other queries. They run in the order they were made, so a later query sees what an earlier one in the group did:
js await sql.begin(async sql => { const [movie] = await sql
select * from movie where name = 'Star Wars'

await Promise.all([ sqlinsert into movie_actor ...${ movie.id }, sqlinsert into movie_release ...${ movie.id } ])

await sqlselect ... })

#### SAVEPOINT await sql.savepoint([name], fn) -> fn()
js sql.begin('read write', async sql => { const [user] = await sql insert into users ( name ) values ( 'Murray' )

const [account] = (await sql.savepoint(sql => sql insert into accounts ( user_id ) values ( ${ user.user_id } ) ).catch(err => { // Account could not be created. ROLLBACK SAVEPOINT is called because we caught the rejection. })) || []

return [user, account] }) .then(([user, account]) => { // great success - COMMIT succeeded }) .catch(() => { // not so good - ROLLBACK was called })

#### Nested sql.begin

Inside a transaction, sql.begin(fn) runs fn in a savepoint, so code that opens a transaction works the same whether it is given the pool or a transaction. Nothing it does is committed until the outer transaction commits. A mode other than the transaction's own, such as 'read only' inside a 'read write' transaction, throws NESTED_TRANSACTION_MODE, since a savepoint can't change it, and so does sql.prepare() inside a nested begin. A savepoint with a name is still sql.savepoint(name, fn).

js async function transfer(sql, from, to, amount) { return sql.begin(async sql => { await sql
update accounts set balance = balance - ${ amount } where id = ${ from } await sqlupdate accounts set balance = balance + ${ amount } where id = ${ to } }) }

await transfer(sql, 1, 2, 10) // its own transaction await sql.begin(sql => transfer(sql, 1, 2, 10)) // a savepoint in this one

#### ROLLBACK await sql.rollback()

Rolls back the transaction without throwing, and sql.begin resolves with what the callback returns (#500). Called in a savepoint or a nested sql.begin, it rolls back only that savepoint, and the outer transaction goes on. The rollback is sent at once, after any queries already made. Queries made after it in the same scope are not sent and reject with TRANSACTION_ROLLED_BACK, and so does the sql.begin they were made in.

js const result = await sql.begin(async sql => { const [order] = await sql
insert into orders ${ sql(input) } returning * if (!await inStock(sql, order)) return (await sql.rollback(), { saved: false }) return { saved: true, order } })
#### PREPARE TRANSACTION await sql.prepare([name]) -> fn()

Indicates that the transactions should be prepared using the [PREPARE TRANSACTION [NAME]](https://www.postgresql.org/docs/current/sql-prepare-transaction.html) statement instead of being committed.

js sql.begin('read write', async sql => { const [user] = await sql
insert into users ( name ) values ( 'Murray' )

await sql.prepare('tx1') })

## Data Transformation

Postgres.js allows for transformation of the data passed to or returned from a query by using the transform option.

Built in transformation functions are:

  • For camelCase - postgres.camel, postgres.toCamel, postgres.fromCamel
  • For PascalCase - postgres.pascal, postgres.toPascal, postgres.fromPascal
  • For Kebab-Case - postgres.kebab, postgres.toKebab, postgres.fromKebab
These built in transformations will only convert to/from snake_case. For example, using { transform: postgres.toCamel } will convert the column names to camelCase only if the column names are in snake_case to begin with. { transform: postgres.fromCamel } will convert camelCase only to snake_case.

By default, using postgres.camel, postgres.pascal and postgres.kebab will perform a two-way transformation - both the data passed to the query and the data returned by the query will be transformed:

js // Transform the column names to and from camel case const sql = postgres({ transform: postgres.camel })

await sqlCREATE TABLE IF NOT EXISTS camel_case (a_test INTEGER, b_test TEXT) await sqlINSERT INTO camel_case ${ sql([{ aTest: 1, bTest: 1 }]) } const data = await sqlSELECT ${ sql('aTest', 'bTest') } FROM camel_case

console.log(data) // [ { aTest: 1, bTest: '1' } ]

To only perform half of the transformation (eg. only the transformation to or from camel case), use the other transformation functions:
js // Transform the column names only to camel case // (for the results that are returned from the query) postgres({ transform: postgres.toCamel })

await sqlCREATE TABLE IF NOT EXISTS camel_case (a_test INTEGER) await sqlINSERT INTO camel_case ${ sql([{ a_test: 1 }]) } const data = await sqlSELECT a_test FROM camel_case

console.log(data) // [ { aTest: 1 } ]

js // Transform the column names only from camel case // (for interpolated inserts, updates, and selects) const sql = postgres({ transform: postgres.fromCamel })

await sqlCREATE TABLE IF NOT EXISTS camel_case (a_test INTEGER) await sqlINSERT INTO camel_case ${ sql([{ aTest: 1 }]) } const data = await sqlSELECT ${ sql('aTest') } FROM camel_case

console.log(data) // [ { a_test: 1 } ]

> Note that Postgres.js does not rewrite the static parts of the tagged template strings. So to transform column names in your queries, the sql() helper must be used - eg. ${ sql('columnName') } as in the examples above.

Transform undefined Values

By default, Postgres.js will throw the error UNDEFINED_VALUE: Undefined values are not allowed when undefined values are passed

js // Transform the column names to and from camel case const sql = postgres({ transform: { undefined: null } })

await sqlCREATE TABLE IF NOT EXISTS transform_undefined (a_test INTEGER) await sqlINSERT INTO transform_undefined ${ sql([{ a_test: undefined }]) } const data = await sqlSELECT a_test FROM transform_undefined

console.log(data) // [ { a_test: null } ]

To combine with the built in transform functions, spread the transform in the transform object:
js // Transform the column names to and from camel case const sql = postgres({ transform: { ...postgres.camel, undefined: null } })

await sqlCREATE TABLE IF NOT EXISTS transform_undefined (a_test INTEGER) await sqlINSERT INTO transform_undefined ${ sql([{ aTest: undefined }]) } const data = await sqlSELECT ${ sql('aTest') } FROM transform_undefined

console.log(data) // [ { aTest: null } ]

### Custom Transform Functions

To specify your own transformation functions, you can use the column, value and row options inside of transform, each an object possibly including to and from keys:

  • to: The function to transform the outgoing query column name to, i.e SELECT ${ sql('aName') } to SELECT a_name when using postgres.toCamel.
  • from: The function to transform the incoming query result column name to, see example below.
> Both parameters are optional, if not provided, the default transformation function will be used.
js // Implement your own functions, look at postgres.toCamel, etc // as a reference: // toCamel and the others in types.js function transformColumnToDatabase() { / ... / } function transformColumnFromDatabase() { / ... / }

const sql = postgres({ transform: { column: { to: transformColumnToDatabase, from: transformColumnFromDatabase, }, value: { / ... / }, row: { / ... / } } })

## Listen & notify

When you call .listen, a dedicated connection will be created to ensure that you receive notifications instantly. This connection will be used for any further calls to .listen. The connection will automatically reconnect according to a backoff reconnection pattern to not overload the database server.

Listen await sql.listen(channel, onnotify, [onlisten]) -> { state }

.listen takes the channel name, a function to handle each notify, and an optional function to run every time listen is registered and ready (happens on initial connect and reconnects). It returns a promise which resolves once the LISTEN query to Postgres completes, or if there is already a listener active.
js await sql.listen('news', payload => { const json = JSON.parse(payload) console.log(json.this) // logs 'is' })
The optional onlisten method is great to use for a very simply queue mechanism:
js await sql.listen( 'jobs', (x) => run(JSON.parse(x)), ( ) => sql
select unfinished_jobs().forEach(run) )

function run(job) { // And here you do the work you please }

### Notify await sql.notify(channel, payload) -> Result[]
Notify can be done as usual in SQL, or by using the sql.notify method.
js sql.notify('news', JSON.stringify({ no: 'this', is: 'news' }))
## Realtime subscribe

Postgres.js implements the logical replication protocol of PostgreSQL to support subscription to real-time updates of insert, update, delete and truncate operations.

> NOTE To make this work you must create the proper publications in your database, enable logical replication by setting wal_level = logical in postgresql.conf and connect using either a replication or superuser.

Quick start

Create a publication (eg. in migration)

sql CREATE PUBLICATION alltables FOR ALL TABLES
#### Subscribe to updates
js const sql = postgres({ publications: 'alltables' })

const { unsubscribe } = await sql.subscribe( 'insert:events', (row, { command, relation, key, old, lsn, time, xid }) => { // Callback function for each row change // tell about new event row over eg. websockets or do something else }, () => { // Callback on initial connect and potential reconnects }, error => { // A handler failed, or the stream had an error } )

Besides the row, each change comes with the transaction it was part of: its lsn (like '0/1A2B3C4'), its commit time and its xid. An update also has old when the table's replica identity sends it, and a truncate has no row but cascade and restartIdentity.

Handlers that return promises

A handler may be async. Changes are then handled one after the other, in the order they were committed, and the stream waits while a handler is busy instead of piling changes up in memory:

js await sql.subscribe('insert:orders', async row => { await queue.publish('order', row) })
### Persistent slots

By default a subscription uses a temporary replication slot, which the server drops when the connection closes, so changes made while the subscriber is away are not seen.

Give the slot a name to keep it. It is created if it does not exist, and streaming starts where it was last confirmed, so nothing committed while the subscriber was down is missed:

js const sql = postgres({ publications: 'alltables', slot: 'orders_worker' })

await sql.subscribe('*:orders', async(row, info) => { await handle(row, info) })

With a slot, a transaction is confirmed to the server only after every handler for it has finished. If a handler throws or rejects, the transaction is not confirmed: onerror is called, the stream restarts and the transaction is delivered again. Delivery is at least once, so a handler should cope with seeing a transaction twice, for instance by remembering the lsn it handled last.

While nothing is in flight, the slot is also confirmed as far as the server has got, so a quiet slot does not keep the server holding WAL.

A persistent slot keeps WAL on the server until it is confirmed, so drop it when it is no longer used:

sql select pg_drop_replication_slot('orders_worker')
### Subscribe pattern

You can subscribe to specific operations, tables, or even rows with primary keys.

operation : schema . table = primary_key

operation is one of

| insert | update | delete | truncate
` and defaults to

schema defaults to public

table is a specific table name and defaults to *

primary_key can be used to only subscribe to specific rows

Examples

sql.subscribe('',                () => / everything */ )
sql.subscribe('insert',           () => / all inserts / )
sql.subscribe(':users',          () => / all operations on the public.users table */ )
sql.subscribe('delete:users',     () => / all deletes on the public.users table / )
sql.subscribe('update:users=1',   () => / all updates on the users row with a primary key = 1 / )
sql.subscribe('truncate:users',   () => / the public.users table was truncated / )

Numbers, bigint, numeric

Number in javascript is only able to represent 253-1 safely which means that types in PostgreSQLs like bigint and numeric won't fit into Number.

Since Node.js v10.4 we can use BigInt to match the PostgreSQL type bigint which is returned for eg. count(*). Unfortunately, it doesn't work with JSON.stringify out of the box, so Postgres.js will return it as a string.

If you want to use BigInt you can add this custom type:

const sql = postgres({
  types: {
    bigint: postgres.BigInt
  }
})

There is currently no guaranteed way to handle numeric / decimal types in native Javascript. These [and similar] types will be returned as a string. The best way in this case is to use custom types.

Result Array

The Result Array returned from queries is a custom array allowing for easy destructuring or passing on directly to JSON.stringify or general Array usage. It includes the following properties.

.count

The count property is the number of affected rows returned by the database. This is useful for insert, update and delete operations to know the number of rows since .length will be 0 in these cases if not using RETURNING ....

.command

The command run by the query - eg. one of SELECT, UPDATE, INSERT, DELETE

.columns

The columns returned by the query useful to determine types, or map to the result values when using .values()

{
  name  : String,    // Column name,
  type  : oid,       // PostgreSQL oid column type
  parser: Function   // The function used by Postgres.js for parsing
}

.statement

The statement contains information about the statement implicitly created by Postgres.js.

{
  name    : String,  // The auto generated statement name
  string  : String,  // The actual query string executed
  types   : [oid],   // An array of oid expected as input parameters
  columns : [Column] // Array of columns - same as Result.columns
}

.state

This is the state { pid, secret } of the connection that executed the query.

Connection details

All Postgres options

const sql = postgres('postgres://username:password@host:port/database', {
  host                 : '',            // Postgres ip address[es] or domain name[s]
  port                 : 5432,          // Postgres server port[s]
  path                 : '',            // unix socket path (usually '/tmp')
  database             : '',            // Name of database to connect to
  username             : '',            // Username of database user, or a function returning it
  password             : '',            // Password of database user, or a function returning it
  ssl                  : false,         // true, prefer, require, verify-full, tls.connect options
  sslnegotiation       : null,          // 'direct' for TLS from the first byte (PostgreSQL 17+)
  channel_binding      : 'prefer',      // SCRAM-SHA-256-PLUS over TLS: 'disable', 'prefer' or 'require'
  max                  : 10,            // Max number of connections
  min                  : 0,             // Connections kept open once the pool is used
  max_pipeline         : 100,           // Max number of queries pipelined on one connection
  scheduler            : true,          // Pipeline where a query should be done soonest (more info below)
  max_lifetime         : null,          // Max lifetime in seconds (more info below)
  idle_timeout         : 0,             // Idle connection timeout in seconds
  connect_timeout      : 30,            // Connect timeout in seconds
  queue_timeout        : 0,             // Seconds a query may wait for a connection, 0 for no limit
  keep_alive           : 60,            // TCP keep-alive in seconds, 0 or null for none
  backoff              : fn,            // Seconds to wait before reconnecting, given the attempt
  prepare              : true,          // Automatic creation of prepared statements
  infer                : false,         // true types parameters from JS values, as v3 did
  types                : [],            // Array of custom types, see more below
  onnotice             : fn,            // Default console.log, set false to silence NOTICE
  onnotify             : fn,            // (channel, payload, pid) for every notification
  onparameter          : fn,            // (key, value) when server param change
  onclose              : fn,            // (connection id) when a connection closes
  debug                : false,         // true, or a function called with (connection, query, params, types)
  socket               : fn,            // (options, host, port) returning a custom socket to use
  transform            : {
    undefined          : undefined,     // Transforms undefined values (eg. to null)
    column             : fn,            // Transforms incoming column names
    value              : fn,            // Transforms incoming row values
    row                : fn             // Transforms entire rows
  },
  connection           : {
    application_name   : 'postgres.js', // Default application_name
    ...                                 // Other connection parameters, see https://www.postgresql.org/docs/current/runtime-config-client.html
  },
  target_session_attrs : null,          // Use 'read-write' with multiple hosts to
                                        // ensure only connecting to primary
  fetch_types          : true,          // Fetches array types and other types it does not know
  publications         : 'alltables',   // Publications for sql.subscribe
  slot                 : null,          // A permanent replication slot for sql.subscribe
  onread               : true,          // Read into one shared buffer per connection (Node.js, Bun, Deno)
  unref_sockets        : false          // Idle connections don't keep the process alive
})

Note that max_lifetime = 60 (30 + Math.random() 30) by default. This resolves to an interval between 30 and 60 minutes to optimize for the benefits of prepared statements and working nicely with Linux's OOM killer.

Dynamic passwords

When clients need to use alternative authentication schemes such as access tokens or connections to databases with rotating passwords, provide either a synchronous or asynchronous function that will resolve the dynamic password value at connection time.

const sql = postgres(url, {
  // Other connection config
  ...
  // Password function for the database user
  password : async () => await signer.getAuthToken(),
})

The user can be a function as well, for credentials that rotate both, such as those from a secrets manager. It is called each time a connection opens, before the password. Give database too, since it can no longer default to the user name.

const sql = postgres({
  database: 'app',
  username: async () => (await secrets.get()).username,
  password: async () => (await secrets.get()).password
})

Password file

With no password given, Postgres.js reads one from the file in PGPASSFILE, or ~/.pgpass (%APPDATA%\postgresql\pgpass.conf on Windows), as libpq does: lines of host:port:database:user:password, where * matches anything and \ escapes a : or \. On Unix the file is ignored unless only its owner can read it (chmod 600).

SSL

Although vulnerable to MITM attacks, a common configuration for the ssl option for some cloud providers is to set rejectUnauthorized to false (if NODE_ENV is production):

const sql =
  process.env.NODE_ENV === 'production'
    ? // "Unless you're using a Private or Shield Heroku Postgres database, Heroku Postgres does not currently support verifiable certificates"
      // https://help.heroku.com/3DELT3RK/why-can-t-my-third-party-utility-connect-to-heroku-postgres-with-ssl
      postgres({ ssl: { rejectUnauthorized: false } })
    : postgres()

For more information regarding ssl with postgres, check out the Node.js documentation for tls.

The modes follow libpq's sslmode: require, prefer and allow encrypt without checking the certificate, and verify-full (or sslrootcert=system in the url) checks it against the system's certificate authorities. prefer and allow fall back to an unencrypted connection when the server has no TLS. Cloudflare Workers always check the certificate.

Direct TLS

PostgreSQL 17 and later can start TLS on the first byte instead of being asked first, which saves a round trip when connecting:

const sql = postgres('postgres://host/db?sslmode=require&sslnegotiation=direct')
// or
const sql = postgres({ ssl: 'require', sslnegotiation: 'direct' })

On Node.js this is also the fastest encrypted connection, as it reads into the same shared buffer as a plain one. It needs a server that supports it, and is not available in Cloudflare Workers, whose sockets cannot send the ALPN it requires.

Channel binding

Over TLS, SCRAM authentication is bound to the server's certificate with SCRAM-SHA-256-PLUS (tls-server-end-point) when the server offers it, so a man in the middle holding some other certificate that looks valid can't relay the login. channel_binding works as in libpq, and can also be given in the url or as PGCHANNELBINDING:

  • 'prefer' (the default) binds when the server offers it, and authenticates as before when it doesn't.
  • 'require' refuses any connection that isn't bound, so a server that isn't reached over TLS, doesn't offer it or authenticates without SCRAM is rejected with CHANNEL_BINDING_REQUIRED.
  • 'disable' never binds.
const sql = postgres('postgres://host/db?sslmode=verify-full&channel_binding=require')

Certificates signed with Ed25519, for which binding isn't defined, are not bound. Cloudflare Workers sockets don't expose the server certificate, so there 'prefer' doesn't bind and 'require' is rejected.

Multi-host connections - High Availability (HA)

Multiple connection strings can be passed to postgres() in the form of postgres('postgres://localhost:5432,localhost:5433', ...). This works the same as native the psql command. Read more at multiple host URIs.

Connections will be attempted in order of the specified hosts/ports. On a successful connection, all retries will be reset. This ensures that hosts can come up and down seamlessly.

If you specify target_session_attrs: 'primary' or PGTARGETSESSIONATTRS=primary Postgres.js will only connect to the primary host, allowing for zero downtime failovers.

The Connection Pool

Connections are created lazily once a query is created. This means that simply doing const sql = postgres(...) won't have any effect other than instantiating a new sql instance.

No connection will be made until a query is made.

For example:

const sql = postgres() // no connections are opened

await sql... // one connection is now opened await sql... // previous opened connection is reused

// two connections are opened now await Promise.all([ sql..., sql... ])

When there are high amount of concurrent queries, postgres will open as many connections as needed up until max number of connections is reached. By default max is 10. This can be changed by setting max in the postgres() call. Example - postgres('connectionURL', { max: 20 }).

This means that we get a much simpler story for error handling and reconnections. Queries will be sent over the wire immediately on the next available connection in the pool. Connections are automatically taken out of the pool if you start a transaction using sql.begin()`, and automatically returned to the pool once your transaction is done.

Any query which was already sent over the wire will be rejected if the connection is lost. It'll automatically defer to the error handling you have for that query, and since connections are lazy it'll automatically try to reconnect the next time a query is made. Th

... (README truncated for length)

Chat with me