Skip to content
Migrating from NextAuth.js v4? Read our migration guide.

Kysely Adapter

Resources

Setup

Installation

npm install kysely @auth/kysely-adapter

Environment Variables

DATABASE_HOST=
DATABASE_NAME=
DATABASE_USER=
DATABASE_PASSWORD=

Configuration

This adapter supports the same first party dialects that Kysely (as of v0.24.2) supports: PostgreSQL, MySQL, and SQLite. The examples below use PostgreSQL with the pg client.

npm install pg
npm install --save-dev @types/pg
./auth.ts
import NextAuth from "next-auth"
import { KyselyAdapter } from "@auth/kysely-adapter"
import { db } from "../../../db"
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: KyselyAdapter(db),
  providers: [],
})

Kysely’s constructor requires a database interface that contains an entry with an interface for each of your tables. You can define these types manually, or use kysely-codegen / prisma-kysely to automatically generate them. Check out the default models required by Auth.js.

db.ts
import { PostgresDialect } from "kysely"
import { Pool } from "pg"
 
// This adapter exports a wrapper of the original `Kysely` class called `KyselyAuth`,
// that can be used to provide additional type-safety.
// While using it isn't required, it is recommended as it will verify
// that the database interface has all the fields that Auth.js expects.
import { KyselyAuth } from "@auth/kysely-adapter"
 
import type { GeneratedAlways } from "kysely"
 
interface Database {
  User: {
    id: GeneratedAlways<string>
    name: string | null
    email: string
    emailVerified: Date | null
    image: string | null
  }
  Account: {
    id: GeneratedAlways<string>
    userId: string
    type: string
    provider: string
    providerAccountId: string
    refresh_token: string | null
    access_token: string | null
    expires_at: number | null
    token_type: string | null
    scope: string | null
    id_token: string | null
    session_state: string | null
  }
  Session: {
    id: GeneratedAlways<string>
    userId: string
    sessionToken: string
    expires: Date
  }
  VerificationToken: {
    identifier: string
    token: string
    expires: Date
  }
}
 
export const db = new KyselyAuth<Database>({
  dialect: new PostgresDialect({
    pool: new Pool({
      host: process.env.DATABASE_HOST,
      database: process.env.DATABASE_NAME,
      user: process.env.DATABASE_USER,
      password: process.env.DATABASE_PASSWORD,
    }),
  }),
})
💡

An alternative to manually defining types is generating them from the database schema using kysely-codegen, or from Prisma schemas using prisma-kysely. When using generated types with KyselyAuth, import Codegen and pass it as the second generic arg:

import type { Codegen } from "@auth/kysely-adapter"
new KyselyAuth<Database, Codegen>()

Schema

db/migrations/001_create_db.ts
import { Kysely, sql } from "kysely"
 
export async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable("User")
    .addColumn("id", "uuid", (col) =>
      col.primaryKey().defaultTo(sql`gen_random_uuid()`)
    )
    .addColumn("name", "text")
    .addColumn("email", "text", (col) => col.unique().notNull())
    .addColumn("emailVerified", "timestamptz")
    .addColumn("image", "text")
    .execute()
 
  await db.schema
    .createTable("Account")
    .addColumn("id", "uuid", (col) =>
      col.primaryKey().defaultTo(sql`gen_random_uuid()`)
    )
    .addColumn("userId", "uuid", (col) =>
      col.references("User.id").onDelete("cascade").notNull()
    )
    .addColumn("type", "text", (col) => col.notNull())
    .addColumn("provider", "text", (col) => col.notNull())
    .addColumn("providerAccountId", "text", (col) => col.notNull())
    .addColumn("refresh_token", "text")
    .addColumn("access_token", "text")
    .addColumn("expires_at", "bigint")
    .addColumn("token_type", "text")
    .addColumn("scope", "text")
    .addColumn("id_token", "text")
    .addColumn("session_state", "text")
    .execute()
 
  await db.schema
    .createTable("Session")
    .addColumn("id", "uuid", (col) =>
      col.primaryKey().defaultTo(sql`gen_random_uuid()`)
    )
    .addColumn("userId", "uuid", (col) =>
      col.references("User.id").onDelete("cascade").notNull()
    )
    .addColumn("sessionToken", "text", (col) => col.notNull().unique())
    .addColumn("expires", "timestamptz", (col) => col.notNull())
    .execute()
 
  await db.schema
    .createTable("VerificationToken")
    .addColumn("identifier", "text", (col) => col.notNull())
    .addColumn("token", "text", (col) => col.notNull().unique())
    .addColumn("expires", "timestamptz", (col) => col.notNull())
    .execute()
 
  await db.schema
    .createIndex("Account_userId_index")
    .on("Account")
    .column("userId")
    .execute()
 
  await db.schema
    .createIndex("Session_userId_index")
    .on("Session")
    .column("userId")
    .execute()
}
 
export async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable("Account").ifExists().execute()
  await db.schema.dropTable("Session").ifExists().execute()
  await db.schema.dropTable("User").ifExists().execute()
  await db.schema.dropTable("VerificationToken").ifExists().execute()
}

This schema is adapted for use in Kysely and is based upon our main schema.

For more information about creating and running migrations with Kysely, refer to the Kysely migrations documentation.

Naming conventions

If mixed snake_case and camelCase column names is an issue for you and/or your underlying database system, we recommend using Kysely’s CamelCasePlugin (see the documentation here) feature to change the field names. This won’t affect NextAuth.js, but will allow you to have consistent casing when using Kysely.

Auth.js © Balázs Orbán and Team - 2024