Installation
1
Install Drizzle ORM and the Better Auth adapter
- npm
- pnpm
- yarn
- bun
npm install drizzle-orm drizzle-kit
pnpm add drizzle-orm drizzle-kit
yarn add drizzle-orm drizzle-kit
bun add drizzle-orm drizzle-kit
2
Configure the adapter
Import
drizzleAdapter from better-auth/adapters/drizzle and pass your Drizzle db instance:auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./database";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // "pg" | "mysql" | "sqlite"
}),
});
Configuration options
ThedrizzleAdapter function accepts a Drizzle db instance and a config object:
| Option | Type | Description |
|---|---|---|
provider | "pg" | "mysql" | "sqlite" | The database provider. Required. |
schema | Record<string, any> | Override the auto-detected schema. Useful when table names differ from model names. |
usePlural | boolean | Set to true when all your table exports use plural names (e.g. users instead of user). |
camelCase | boolean | Use camelCase for generated field names instead of snake_case. Defaults to false. |
debugLogs | boolean | Enable verbose adapter logging. Defaults to false. |
transaction | boolean | Wrap multi-step operations in a database transaction. Defaults to false. |
Schema generation
The Better Auth CLI generates the required Drizzle schema based on your auth configuration and any plugins you have enabled.npx auth@latest generate
- generate migration
- apply migration
npx drizzle-kit generate
npx drizzle-kit migrate
Schema examples
The tables below are whatnpx auth@latest generate produces for each provider.
- PostgreSQL
- MySQL
- SQLite
schema.ts
import { relations } from "drizzle-orm";
import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
});
export const session = pgTable(
"session",
{
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => [index("session_userId_idx").on(table.userId)],
);
export const account = pgTable(
"account",
{
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [index("account_userId_idx").on(table.userId)],
);
export const verification = pgTable(
"verification",
{
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [index("verification_identifier_idx").on(table.identifier)],
);
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
}));
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, { fields: [session.userId], references: [user.id] }),
}));
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, { fields: [account.userId], references: [user.id] }),
}));
schema.ts
import { relations } from "drizzle-orm";
import {
mysqlTable,
varchar,
text,
timestamp,
boolean,
index,
} from "drizzle-orm/mysql-core";
export const user = mysqlTable("user", {
id: varchar("id", { length: 36 }).primaryKey(),
name: varchar("name", { length: 255 }).notNull(),
email: varchar("email", { length: 255 }).notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { fsp: 3 })
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
});
export const session = mysqlTable(
"session",
{
id: varchar("id", { length: 36 }).primaryKey(),
expiresAt: timestamp("expires_at", { fsp: 3 }).notNull(),
token: varchar("token", { length: 255 }).notNull().unique(),
createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { fsp: 3 })
.$onUpdate(() => new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: varchar("user_id", { length: 36 })
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => [index("session_userId_idx").on(table.userId)],
);
export const account = mysqlTable(
"account",
{
id: varchar("id", { length: 36 }).primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: varchar("user_id", { length: 36 })
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at", { fsp: 3 }),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { fsp: 3 }),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { fsp: 3 })
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [index("account_userId_idx").on(table.userId)],
);
export const verification = mysqlTable(
"verification",
{
id: varchar("id", { length: 36 }).primaryKey(),
identifier: varchar("identifier", { length: 255 }).notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at", { fsp: 3 }).notNull(),
createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { fsp: 3 })
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [index("verification_identifier_idx").on(table.identifier)],
);
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
}));
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, { fields: [session.userId], references: [user.id] }),
}));
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, { fields: [account.userId], references: [user.id] }),
}));
schema.ts
import { relations, sql } from "drizzle-orm";
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
export const user = sqliteTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: integer("email_verified", { mode: "boolean" })
.default(false)
.notNull(),
image: text("image"),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.$onUpdate(() => new Date())
.notNull(),
});
export const session = sqliteTable(
"session",
{
id: text("id").primaryKey(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
token: text("token").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.$onUpdate(() => new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => [index("session_userId_idx").on(table.userId)],
);
export const account = sqliteTable(
"account",
{
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: integer("access_token_expires_at", {
mode: "timestamp_ms",
}),
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
mode: "timestamp_ms",
}),
scope: text("scope"),
password: text("password"),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [index("account_userId_idx").on(table.userId)],
);
export const verification = sqliteTable(
"verification",
{
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [index("verification_identifier_idx").on(table.identifier)],
);
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
}));
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, { fields: [session.userId], references: [user.id] }),
}));
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, { fields: [account.userId], references: [user.id] }),
}));
Modifying table names
If your Drizzle schema uses different table export names (for exampleusers instead of user), pass the schema object and remap the key:
auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./drizzle";
import * as schema from "./schema";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema: {
...schema,
user: schema.users, // remap plural export to the singular model name
},
}),
});
modelName directly in the auth config:
auth.ts
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg", schema }),
user: {
modelName: "users",
},
});
Modifying field names
Better Auth maps field names using the property name you assign in your Drizzle schema. To rename a column in the database without changing the property name, update the column string argument:export const user = mysqlTable("user", {
// The JS property stays `email`; the DB column becomes `email_address`
email: varchar("email_address", { length: 255 }).notNull().unique(),
});
auth.ts
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "mysql", schema }),
user: {
fields: {
email: "email_address",
},
},
});
Using plural table names
If every table in your schema uses a plural name, passusePlural: true instead of remapping each model:
auth.ts
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema,
usePlural: true,
}),
});
Experimental joins
Enabling joins allows Better Auth to fetch related data in a single query rather than issuing multiple round-trips. Endpoints such as/get-session and /get-full-organization see 2-3x latency improvements.
auth.ts
export const auth = betterAuth({
experimental: { joins: true },
});
Joins require Drizzle
relations definitions in your schema. Run npx auth@latest generate with the latest CLI to produce a schema that includes them. You must also pass each relation through the adapter’s schema object.