Skip to Content

Last Updated: 3/20/2026


Supabase Integration

Supabase is an open-source Firebase alternative built on PostgreSQL. While Supabase provides its own JavaScript client (@supabase/supabase-js) that wraps a PostgREST API, you can use Kysely to write type-safe SQL queries directly against your Supabase PostgreSQL database.

Why Use Kysely with Supabase

Supabase’s JavaScript client is convenient for simple queries, but it has limitations:

  • No raw SQL support: You can’t write custom SQL queries
  • Limited query complexity: Complex joins, subqueries, and CTEs are difficult or impossible
  • PostgREST constraints: The API layer adds overhead and restricts what you can do

Kysely gives you direct PostgreSQL access with full SQL capabilities while maintaining type safety. You can use Kysely alongside Supabase’s client, choosing the right tool for each query.

Prerequisites

Before integrating Kysely with Supabase, you need:

  1. Supabase project: Create a project at supabase.com or use the Supabase CLI for local development
  2. Supabase CLI: Install supabase CLI for type generation
  3. Kysely: Install kysely package
  4. PostgreSQL driver: Install pg or postgres (with kysely-postgres-js)
  5. kysely-supabase: Install the type translation package
npm install kysely pg npm install -D kysely-supabase

Generating Types with Supabase CLI

Supabase can generate TypeScript types from your database schema. Run this command to generate types:

npx supabase gen types typescript --local > src/database.types.ts

For a remote Supabase project, use:

npx supabase gen types typescript --project-id YOUR_PROJECT_ID > src/database.types.ts

This creates a Database type that describes your database schema in Supabase’s format. The generated file includes types for tables, views, functions, and enums.

Translating Supabase Types to Kysely Types

Supabase’s generated types use a different structure than Kysely expects. The kysely-supabase package provides the KyselifyDatabase utility type to translate between formats:

// src/types.ts import type { Database as SupabaseDatabase } from './database.types' import type { KyselifyDatabase } from 'kysely-supabase' export type Database = KyselifyDatabase<SupabaseDatabase>

The KyselifyDatabase type transforms Supabase’s nested structure into Kysely’s flat table-to-columns mapping. It handles:

  • Table types: Converts Supabase’s Tables structure to Kysely’s table interface
  • Column types: Maps Supabase’s Row, Insert, and Update types to Kysely’s column types
  • Nullable columns: Preserves nullability information
  • Generated columns: Marks auto-generated columns appropriately

Connecting to Supabase

Use your Supabase connection string with Kysely’s PostgreSQL dialect:

// src/db.ts import { Kysely, PostgresDialect } from 'kysely' import { Pool } from 'pg' import type { Database } from './types' export const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL, }), }), })

Get your connection string from the Supabase dashboard under Settings → Database → Connection string. Use the “Connection pooling” string for serverless environments.

Important: Supabase provides two connection strings:

  • Direct connection: For long-lived connections (traditional servers)
  • Connection pooling: For serverless functions and edge environments

Use the pooling connection string with Supabase’s connection pooler (port 6543) for serverless deployments.

Using postgres Driver

For serverless environments, the postgres driver is often more efficient than pg:

npm install postgres kysely-postgres-js
import { Kysely } from 'kysely' import { PostgresJSDialect } from 'kysely-postgres-js' import postgres from 'postgres' import type { Database } from './types' export const db = new Kysely<Database>({ dialect: new PostgresJSDialect({ postgres: postgres(process.env.DATABASE_URL), }), })

The postgres driver is designed for serverless environments and handles connection pooling more efficiently than pg.

Querying with Type Safety

Once configured, use Kysely normally. The types from Supabase ensure your queries are type-safe:

// Assuming your Supabase database has a 'person' table const people = await db .selectFrom('person') .select(['id', 'first_name', 'last_name']) .where('age', '>', 18) .execute() // TypeScript knows the shape of 'people' people.forEach(person => { console.log(person.first_name) // Type-safe })

Insert, update, and delete queries are also type-safe:

await db .insertInto('person') .values({ first_name: 'John', last_name: 'Doe', age: 30, }) .execute() await db .updateTable('person') .set({ age: 31 }) .where('id', '=', 1) .execute()

TypeScript validates that column names exist and that values have the correct types.

Working with Supabase Features

Kysely works with Supabase-specific PostgreSQL features:

Row Level Security (RLS): Kysely respects RLS policies. Set the user context before querying:

import { sql } from 'kysely' await db.transaction().execute(async (trx) => { // Set the user context for RLS await sql`SELECT set_config('request.jwt.claims', '${sql.raw(JSON.stringify({ sub: userId }))}', true)`.execute(trx) // Queries now respect RLS policies for this user const data = await trx .selectFrom('private_table') .selectAll() .execute() return data })

Realtime subscriptions: Use Supabase’s client for realtime features. Kysely is for direct database queries, not realtime subscriptions.

Storage and Auth: Use Supabase’s client libraries for storage and authentication. Kysely is for database operations only.

Migrations

Supabase manages migrations through its CLI. Create migrations with:

npx supabase migration new create_person_table

Edit the generated SQL file in supabase/migrations/, then apply with:

npx supabase db push

After changing your schema, regenerate types:

npx supabase gen types typescript --local > src/database.types.ts

Your Kysely queries will automatically pick up the new types.

Local Development

Use Supabase’s local development environment:

npx supabase start

This starts a local PostgreSQL instance, PostgREST API, and other Supabase services. Connect Kysely to the local database:

// Use the local connection string const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:54322/postgres', }), }), })

The local connection string is shown when you run supabase start.

Combining Kysely and Supabase Client

You can use both Kysely and Supabase’s client in the same application:

import { createClient } from '@supabase/supabase-js' import { db } from './db' const supabase = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY ) // Use Supabase client for auth const { data: { user } } = await supabase.auth.getUser() // Use Kysely for complex queries const result = await db .selectFrom('person') .innerJoin('pet', 'pet.owner_id', 'person.id') .select(['person.first_name', 'pet.name as pet_name']) .where('person.id', '=', user.id) .execute()

This pattern lets you use Supabase’s auth and realtime features while leveraging Kysely for complex database queries.

Type Generation Workflow

Establish a workflow for keeping types in sync:

  1. Make schema changes in Supabase (via migrations or dashboard)
  2. Apply migrations: npx supabase db push
  3. Regenerate types: npx supabase gen types typescript --local > src/database.types.ts
  4. TypeScript recompiles and catches any breaking changes in your Kysely queries

Add type generation to your CI/CD pipeline to ensure types stay synchronized with your database schema.

What’s Next

  • Dialects: Learn about Kysely’s PostgreSQL dialect and other built-in dialects.
  • Transactions: Use transactions to ensure data consistency in complex operations.
  • Migrations: Explore advanced migration patterns and strategies.