Better Auth + Drizzle: The Schema Gotcha That Breaks Everything

#til #better-auth #drizzle #nextjs

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:

  1. Run npx better-auth generate to generate the auth schema file
  2. 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.

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

  • drizzleAdapter with schema: 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 use db._.fullSchema, which is almost always what you want
  • The error model user not found means the adapter can’t find a table definition, not that the database table is missing
  • Generate BETTER_AUTH_SECRET with openssl rand -hex 32, not -base64