--- id: 'kysely-postgres' title: 'Type-Safe SQL with Kysely' description: 'Combining Kysely with Deno Postgres gives you a convenient developer experience for interacting directly with your Postgres database.' --- Supabase Edge Functions can [connect directly to your Postgres database](/docs/guides/functions/connect-to-postgres) to execute SQL queries. [Kysely](https://github.com/kysely-org/kysely#kysely) is a type-safe and autocompletion-friendly typescript SQL query builder. Combining Kysely with Deno Postgres gives you a convenient developer experience for interacting directly with your Postgres database. ## Code Find the example on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/kysely-postgres) Get your database connection credentials from the project's [**Connect** panel](/dashboard/project/_/?showConnect=true) and store them in an `.env` file: ```bash .env DB_HOSTNAME= DB_PASSWORD= DB_SSL_CERT="-----BEGIN CERTIFICATE----- GET YOUR CERT FROM YOUR PROJECT DASHBOARD -----END CERTIFICATE-----" ``` Create a `DenoPostgresDriver.ts` file to manage the connection to Postgres via [deno-postgres](https://deno-postgres.com/): ```ts DenoPostgresDriver.ts import { Pool, PoolClient } from 'jsr:@db/postgres@^0' import { CompiledQuery, DatabaseConnection, Driver, PostgresCursorConstructor, QueryResult, TransactionSettings, } from 'npm:kysely@^0' import { freeze, isFunction } from 'npm:kysely@^0/dist/esm/util/object-utils.js' import { extendStackTrace } from 'npm:kysely@^0/dist/esm/util/stack-trace-utils.js' export interface PostgresDialectConfig { pool: Pool | (() => Promise) cursor?: PostgresCursorConstructor onCreateConnection?: (connection: DatabaseConnection) => Promise } const PRIVATE_RELEASE_METHOD = Symbol() export class PostgresDriver implements Driver { readonly #config: PostgresDialectConfig readonly #connections = new WeakMap() #pool?: Pool constructor(config: PostgresDialectConfig) { this.#config = freeze({ ...config }) } async init(): Promise { this.#pool = isFunction(this.#config.pool) ? await this.#config.pool() : this.#config.pool } async acquireConnection(): Promise { const client = await this.#pool!.connect() let connection = this.#connections.get(client) if (!connection) { connection = new PostgresConnection(client, { cursor: this.#config.cursor ?? null, }) this.#connections.set(client, connection) // The driver must take care of calling `onCreateConnection` when a new // connection is created. The `pg` module doesn't provide an async hook // for the connection creation. We need to call the method explicitly. if (this.#config?.onCreateConnection) { await this.#config.onCreateConnection(connection) } } return connection } async beginTransaction( connection: DatabaseConnection, settings: TransactionSettings ): Promise { if (settings.isolationLevel) { await connection.executeQuery( CompiledQuery.raw(`start transaction isolation level ${settings.isolationLevel}`) ) } else { await connection.executeQuery(CompiledQuery.raw('begin')) } } async commitTransaction(connection: DatabaseConnection): Promise { await connection.executeQuery(CompiledQuery.raw('commit')) } async rollbackTransaction(connection: DatabaseConnection): Promise { await connection.executeQuery(CompiledQuery.raw('rollback')) } async releaseConnection(connection: PostgresConnection): Promise { connection[PRIVATE_RELEASE_METHOD]() } async destroy(): Promise { if (this.#pool) { const pool = this.#pool this.#pool = undefined await pool.end() } } } interface PostgresConnectionOptions { cursor: PostgresCursorConstructor | null } class PostgresConnection implements DatabaseConnection { #client: PoolClient #options: PostgresConnectionOptions constructor(client: PoolClient, options: PostgresConnectionOptions) { this.#client = client this.#options = options } async executeQuery(compiledQuery: CompiledQuery): Promise> { try { const result = await this.#client.queryObject(compiledQuery.sql, [ ...compiledQuery.parameters, ]) if ( result.command === 'INSERT' || result.command === 'UPDATE' || result.command === 'DELETE' ) { const numAffectedRows = BigInt(result.rowCount || 0) return { numUpdatedOrDeletedRows: numAffectedRows, numAffectedRows, rows: result.rows ?? [], } as any } return { rows: result.rows ?? [], } } catch (err) { throw extendStackTrace(err, new Error()) } } async *streamQuery( _compiledQuery: CompiledQuery, chunkSize: number ): AsyncIterableIterator> { if (!this.#options.cursor) { throw new Error( "'cursor' is not present in your postgres dialect config. It's required to make streaming work in postgres." ) } if (!Number.isInteger(chunkSize) || chunkSize <= 0) { throw new Error('chunkSize must be a positive integer') } // stream not available return null } [PRIVATE_RELEASE_METHOD](): void { this.#client.release() } } ``` Create an `index.ts` file to execute a query on incoming requests: ```ts index.ts import { Pool } from 'jsr:@db/postgres@^0' import { withSupabase } from 'npm:@supabase/server@^1' import { Generated, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler, } from 'npm:kysely@^0' import { PostgresDriver } from './DenoPostgresDriver.ts' console.log(`Function "kysely-postgres" up and running!`) interface AnimalTable { id: Generated animal: string created_at: Date } // Keys of this interface are table names. interface Database { animals: AnimalTable } // Create a database pool with one connection. const pool = new Pool( { tls: { caCertificates: [Deno.env.get('DB_SSL_CERT')!] }, database: 'postgres', hostname: Deno.env.get('DB_HOSTNAME'), user: 'postgres', port: 5432, password: Deno.env.get('DB_PASSWORD'), }, 1 ) // You'd create one of these when you start your app. const db = new Kysely({ dialect: { createAdapter() { return new PostgresAdapter() }, createDriver() { return new PostgresDriver({ pool }) }, createIntrospector(db: Kysely) { return new PostgresIntrospector(db) }, createQueryCompiler() { return new PostgresQueryCompiler() }, }, }) export default { fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => { try { // Run a query const animals = await db .selectFrom('animals') .select(['id', 'animal', 'created_at']) .execute() // Neat, it's properly typed \o/ console.log(animals[0].created_at.getFullYear()) const data = animals.map((animal) => Object.fromEntries( Object.entries(animal).map(([key, value]) => [ key, typeof value === 'bigint' ? value.toString() : value, ]) ) ) return Response.json(data, { headers: { 'Content-Type': 'application/json; charset=utf-8', }, }) } catch (err) { console.error(err) return Response.json({ error: String(err?.message ?? err) }, { status: 500 }) } }), } ```