Better Auth + Drizzle: The Schema Gotcha That Breaks Everything
The error appears at runtime, not build time. Your app compiles fine, your migrations ran, your database has the right tables — and then you hit an auth endpoint and get:
Error: model user not found
Here’s what’s happening and the one-line fix.
The Setup
Stack: Next.js 16, Turbopack, Better Auth, Drizzle ORM, Neon (serverless Postgres).
The Better Auth docs show you how to set up the Drizzle adapter. It looks like this:
// src/lib/auth/index.ts
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { db } from '@/lib/db';
import * as schema from '@/lib/db/schema';
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
schema: schema, // <-- this is the problem
}),
emailAndPassword: { enabled: true },
});
This seems reasonable. You’re passing your schema so the adapter knows about your tables. Makes sense, right?
What’s Actually Happening
When you pass schema to drizzleAdapter, you’re passing your schema — the tables you defined for your application. Better Auth also needs its own tables: user, session, account, and verification. These are created by Better Auth’s migration tooling and should be in your database.
The problem: if your schema file only exports your app’s tables and not Better Auth’s tables, the adapter can’t find them. It looks for user in the schema object you passed, doesn’t find it, and throws model user not found.
The error message describes the symptom but gives no indication of the cause. Nothing tells you “hey, your schema doesn’t include Better Auth’s own tables.”
The Fix
Don’t pass schema at all.
// src/lib/auth/index.ts
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { db } from '@/lib/db';
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
// No schema: here
}),
emailAndPassword: { enabled: true },
});
When you omit schema, drizzleAdapter falls back to db._.fullSchema — the complete schema that Drizzle builds internally, which includes everything: your tables and Better Auth’s tables (as long as they were generated via npx better-auth generate and included in your Drizzle schema).
Before and After
Before (broken):
import * as schema from '@/lib/db/schema';
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
schema: schema,
}),
// ...
});
After (working):
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
}),
// ...
});
That’s the entire change. Two lines removed.
Making Sure Better Auth’s Tables Are in Your Schema
For db._.fullSchema to include Better Auth’s tables, you need to:
- Run
npx better-auth generateto generate the auth schema file - Import it in your Drizzle config so it’s part of the schema Drizzle knows about
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: ['./src/lib/db/schema.ts', './src/lib/db/auth-schema.ts'],
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
The auth-schema.ts file is generated by Better Auth and contains the user, session, account, and verification table definitions. Include it in your schema glob. Don’t cherry-pick it into your main schema.ts — keep it separate so Better Auth can regenerate it without conflicts.
Related Gotcha: BETTER_AUTH_SECRET Format
While you’re setting up Better Auth, one more thing that bites people: BETTER_AUTH_SECRET must be generated with openssl rand -hex 32, not openssl rand -base64 32.
The base64 variant produces characters like +, /, and =. These cause parsing issues in certain environments and can silently break auth operations. Hex is safe:
openssl rand -hex 32
# outputs: a1b2c3d4e5f6... (64 hex chars, no special characters)
TIL
drizzleAdapterwithschema:only sees the tables you explicitly pass — Better Auth’s own tables won’t be there unless you include them- Omitting
schema:makes the adapter usedb._.fullSchema, which is almost always what you want - The error
model user not foundmeans the adapter can’t find a table definition, not that the database table is missing - Generate
BETTER_AUTH_SECRETwithopenssl rand -hex 32, not-base64