Skip to main content
node-postgres (the pg package) is the most widely used Postgres driver for Node.js. In this guide, you connect to ClickHouse Managed Postgres through a pg.Pool with full TLS certificate verification, create two tables, move money between accounts in a transaction, and serve the data from a small node:http server.

Prerequisites

  • Node.js 20.6 or later (for the built-in --env-file flag). This guide was tested with Node.js 24.21.0 and pg 8.23.1.
  • A ClickHouse Cloud account
  • psql, to create the database

Create a ClickHouse Managed Postgres service

In the ClickHouse Cloud console, click New service and select Postgres. The instance is ready in a few minutes. See the quickstart for a walkthrough.

Get your connection details

Click Connect in the left sidebar of your service. With Directly selected, copy the connection URL from the url tab. Leave Use SSL off: the app passes the CA certificate in code instead of in the URL (see Configure the connection pool). This guide connects directly to Postgres on port 5432. A long-lived Node.js server already keeps its own pool of connections through pg.Pool, so it doesn’t need a second pooler in front of Postgres. If you run many processes or serverless functions, use the bundled PgBouncer instead; see Use PgBouncer. Next, download the CA certificate for your instance. Turn on Use SSL and click Download CA certificate, or go to Settings → CA Certificate → Download CA certificate. The certificate is unique to your instance.

Set up the project

Create a project and install pg:
Adding to an existing app?Skip mkdir and npm init -y. In your project, run npm install pg, then continue with Create the database.
The examples in this guide are ES modules with the .mjs extension, so they work whether your package.json uses CommonJS or ES modules.

Create the database

Move the downloaded <service-name>-ca-certificate.pem file into the project directory and rename it to ca-certificate.pem. Then create a database for the app:
Create a .env file with the connection URL you copied, with the database name changed to guide_nodejs, and the path to the certificate:
.env
If your password contains characters such as @, /, or #, percent-encode them in the URL.

Configure the connection pool

Create db.mjs. It exports one pool that the rest of the app shares:
db.mjs
Passing ssl: { ca } gives you the equivalent of sslmode=verify-full: pg checks that the server certificate is signed by your instance’s CA and that it matches the hostname. rejectUnauthorized defaults to true; don’t set it to false.
Don’t mix ssl with SSL parameters in the URLIf DATABASE_URL contains sslmode or sslrootcert, pg builds its TLS settings from the URL and ignores the ssl object. For example, a URL ending in ?sslmode=verify-full without sslrootcert replaces your ca, and the connection fails with unable to verify the first certificate. Keep SSL parameters out of DATABASE_URL when you pass ssl in code.Also note that:
  • Without either an ssl object or sslmode in the URL, pg connects without TLS.
  • pg reads PGHOST, PGUSER, PGPASSWORD, and similar variables, but it doesn’t read PGSSLROOTCERT. The variables in the env tab of the Connect modal therefore aren’t enough on their own to verify the server.
  • If you prefer to keep everything in the URL, the URL that the Connect modal shows with Use SSL on also works with pg, for example ...?sslmode=verify-full&sslrootcert=ca-certificate.pem. In that case, remove the ssl option, and note that sslrootcert is resolved relative to the directory you start Node.js from.

Create the schema

Create setup.mjs. It creates the tables, inserts two accounts with parameterized queries, and reads them back:
setup.mjs
Always pass values as parameters ($1, $2, …) rather than concatenating them into the SQL string. pg sends them separately from the query text, which prevents SQL injection. JavaScript arrays map to Postgres arrays, as in the ANY($1) query. Run it:
pg returns numeric (and bigint) values as strings so that no precision is lost. Convert them explicitly if you need numbers.

Run queries in a transaction

A transaction must run on a single connection, so check out a client with pool.connect, and always release it. Create transfer.mjs:
transfer.mjs
Don’t use pool.query for the statements inside a transaction: each pool.query call can run on a different connection.

Serve the data over HTTP

Create server.mjs. It serves the accounts and transfers as JSON on port 3107, accepts new transfers, and shuts down cleanly on SIGINT (Ctrl+C) or SIGTERM:
server.mjs
Call pool.end once, when the process shuts down, not after each request. It waits for checked-out clients to be released and then closes every connection.

Verify

Start the server:
In a second terminal, make a transfer, then try one that would overdraw an account and one to an account that doesn’t exist:
The third transfer had already debited alice when it failed, but the ROLLBACK undid it. Open http://localhost:3107/accounts in your browser to see the balances. The response looks like this:
To confirm that the connection uses verified TLS, run a query against pg_stat_ssl:
If you point DATABASE_CA_CERT at any other certificate, the connection fails with unable to verify the first certificate, which shows that the server certificate is actually checked. Finally, stop the server with Ctrl+C. It prints SIGINT received, shutting down and then Pool closed. You can also look at the data in the console. Open SQL console, expand the guide_nodejs database, and open the accounts table:

Use PgBouncer

To connect through the bundled PgBouncer, select via PgBouncer in the Connect modal and use port 6432 in DATABASE_URL. No code changes are needed; the same CA certificate works. PgBouncer runs in transaction pooling mode, so each transaction can run on a different Postgres connection. With pg:
  • Parameterized queries, named prepared statements (pool.query({ name, text, values })), and transactions that use pool.connect work as shown in this guide.
  • SQL-level PREPARE and EXECUTE, and session settings made with SET, don’t carry over between transactions. Use SET LOCAL inside a transaction instead.
  • pg_stat_ssl reports the connection between PgBouncer and Postgres, so the TLS check query above returns ssl: false. Your app’s connection to PgBouncer is still verified: a wrong CA certificate fails in the same way.

Next steps

Last modified on September 30, 2026