Skip to main content
Kysely is a type-safe SQL query builder for TypeScript. In this guide, you connect Kysely to ClickHouse Managed Postgres through the pg driver with verified TLS, create a table with Kysely migrations, and serve a small to-do API from Node.js.

Prerequisites

  • Node.js 22.18 or later, which runs TypeScript files directly without a build step. The guide’s files use the .mts extension, so Node.js treats them as ES modules in both CommonJS and ES module projects.
  • A ClickHouse Cloud account.
  • psql, to create the database. You can also run the CREATE DATABASE statement in the SQL console.
  • curl to call the API.
This guide was tested with Node.js 24.21, Kysely 0.29.6, pg 8.23.1, TypeScript 7.0.2, and PostgreSQL 18.

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. Keep Directly selected and turn on Use SSL: the url connection string now ends with sslmode=verify-full&sslrootcert=.... Click Download CA certificate to download your service’s CA certificate, named <service-name>-ca-certificate.pem, and copy the connection string. This guide connects directly to Postgres on port 5432, for two reasons:
  • A Node.js server is a long-lived process. The pg Pool already keeps a small set of connections open and reuses them, so a server-side pooler adds little.
  • Kysely’s Migrator holds a session-level advisory lock (pg_advisory_lock) while it runs migrations. PgBouncer runs in transaction pooling mode, so taking the lock, running the migrations, and releasing the lock can each land on a different backend connection. Through PgBouncer, the lock can stay held on a pooled connection, and later migration runs block waiting for it.
If you later run many app instances or serverless functions, you can point the app’s connection string at PgBouncer (port 6432, same host). Kysely with pg works in transaction pooling mode because pg uses unnamed prepared statements by default. Keep running migrations against port 5432. See PgBouncer connection pooling.

Set up the project

Create a project and install Kysely, the pg driver, and TypeScript:
Adding to an existing app?Skip mkdir kysely-todo && cd kysely-todo and npm init -y. In your project, run the following commands, which add migrate and typecheck scripts but leave your start script alone, then continue with Configure the connection.

Configure the connection

Move the CA certificate you downloaded into your project directory and rename it to ca-certificate.pem. Then create a database for the app:
Add the connection string you copied to a .env file, or to your existing .env. Replace <PASSWORD> with your password, the database name with guide_kysely, and set sslrootcert to ca-certificate.pem:
.env
pg reads sslmode and sslrootcert from the connection string: verify-full makes it check that the server certificate is signed by your service’s CA and matches the hostname. sslrootcert is resolved relative to the working directory, which is the project root when you use npm run.
Keep sslmode=verify-full in the URL. Without sslmode, pg connects without TLS at all. Don’t also pass an ssl object to Pool: when the URL contains sslmode, the URL’s settings replace your ssl object. If you need to load the CA from somewhere other than a file, remove sslmode and sslrootcert from the URL and pass ssl: { ca } to Pool instead.
Add a tsconfig.json so that tsc type-checks the same files that Node.js runs. If your app already has a tsconfig.json, merge these compilerOptions and the migrations folder into it:
tsconfig.json

Define the database interface

Kysely types every query from an interface that describes your tables. Create src/db.mts with the interface and a Kysely instance that uses PostgresDialect with a pg Pool:
src/db.mts
Generated marks columns that the database fills in, so they’re optional on insert.

Create and run a migration

This guide uses Kysely’s built-in Migrator with FileMigrationProvider, which is part of the core kysely package. The separate kysely-ctl CLI is optional and wraps the same Migrator. Create a migration file. Kysely runs migrations in alphanumeric order of their file names, so prefix them with a date:
migrations/2026-09-30_create_todo.mts
Create src/migrate.mts to apply all pending migrations. In Kysely 0.29, import Migrator and FileMigrationProvider from kysely/migration:
src/migrate.mts
Run the migration:
Kysely records applied migrations in the kysely_migration table, so running the command again does nothing.
To confirm that pg really verifies the certificate, run the migration without sslrootcert. Node.js doesn’t trust your service’s CA by default, so the connection fails:

Query the database

Create src/server.mts, a small HTTP server that creates, reads, updates, and deletes to-dos with Kysely. In an existing app, you can use the same queries in your own route handlers:
src/server.mts
Because every query is checked against the Database interface, a misspelled table or column name, or an insert without the required title, fails type-checking. Check the project:

Verify

Start the server:
In an existing app, where npm start runs your own app, start the example server with node --env-file=.env src/server.mts instead. In another terminal, create, update, and delete a few to-dos:
Open http://localhost:3000/todos in your browser to list the remaining to-dos. The response looks like this:
To see the same data in the console, open SQL console in the left sidebar of your service, expand guide_kysely and the public schema, and click the todo table. The kysely_migration and kysely_migration_lock tables that Kysely created are listed next to it:

Next steps

Last modified on September 30, 2026