js

Build Type-Safe Next.js Authentication with Better Auth and Drizzle ORM

Learn how to build secure, type-safe Next.js authentication with Better Auth and Drizzle ORM. Reduce auth bugs and ship with confidence.

Build Type-Safe Next.js Authentication with Better Auth and Drizzle ORM

I was building yet another dashboard application last week when it hit me. I spent three hours debugging a session mismatch that locked users out after password resets. My authentication code had become a fragile house of cards - it worked, but I didn’t trust it. Have you ever felt that unease when your auth logic grows more complex than your actual features?

This frustration led me down a new path. Instead of stitching together middleware, database calls, and token validation manually, I discovered a cleaner approach. The combination of Better Auth and Drizzle ORM creates something remarkable: authentication that feels solid, predictable, and thoroughly checked by TypeScript before you even run the code.

Let me show you what I built. This isn’t just another “add login to your app” tutorial. This is about creating a foundation that won’t crack under pressure.

First, we need to set the stage. Create a new Next.js project with TypeScript enabled:

npx create-next-app@latest my-secure-app --typescript --app --no-tailwind

Now install the core tools. Better Auth handles the complex logic, while Drizzle gives us type-safe database interactions:

npm install better-auth drizzle-orm pg
npm install -D drizzle-kit @types/pg

The database connection is straightforward. Create src/db/index.ts:

import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from './schema'

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
})

export const db = drizzle(pool, { schema })

But here’s where things get interesting. Better Auth doesn’t hide your schema behind magic. You define it explicitly, which means you can extend it with your fields. Look at this user table definition:

// In src/db/schema.ts
export const users = pgTable('users', {
  id: text('id').primaryKey(),
  email: text('email').notNull().unique(),
  name: text('name'),
  role: text('role').notNull().default('user'),
  createdAt: timestamp('created_at').notNull().defaultNow(),
})

See that role field? That’s not part of the default auth setup. I added it because my application needs to distinguish between regular users and administrators. This flexibility matters when your needs grow beyond basic login.

Now, let’s initialize Better Auth. Create src/lib/auth.ts:

import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { db } from '@/db'

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: 'pg'
  }),
  emailAndPassword: {
    enabled: true,
  },
})

What makes this different from other auth libraries? The types flow through your entire application. When you fetch a user session on the server, TypeScript knows exactly what fields exist. No more guessing whether user.email might be undefined.

Let me show you how to protect a page. Create src/app/dashboard/page.tsx:

import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'

export default async function Dashboard() {
  const session = await auth.api.getSession({
    headers: new Headers()
  })
  
  if (!session) {
    redirect('/login')
  }
  
  // TypeScript knows session.user has email, name, and role
  return <div>Welcome back, {session.user.name}</div>
}

Did you notice how clean that is? No complex context providers wrapping your app. No uncertain type assertions. The session is either there with known properties, or it’s not, and we redirect.

But what about social login? Adding OAuth providers feels almost too simple:

export const auth = betterAuth({
  // ... previous config
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }
  }
})

The sign-in component becomes equally straightforward. Here’s a basic login form:

'use client'

import { signInWithEmail } from '@/lib/auth/client'

export function LoginForm() {
  async function handleSubmit(formData: FormData) {
    const result = await signInWithEmail({
      email: formData.get('email') as string,
      password: formData.get('password') as string,
    })
    
    if (result.error) {
      // Handle error - TypeScript knows the possible error types
      console.error(result.error.message)
    }
  }
  
  return (
    <form action={handleSubmit}>
      <input name="email" type="email" />
      <input name="password" type="password" />
      <button type="submit">Sign In</button>
    </form>
  )
}

Here’s a question for you: When was the last time your authentication errors were properly typed? With this setup, error handling becomes predictable instead of guessing game.

Middleware protection is another area where this stack shines. Create src/middleware.ts:

import { auth } from '@/lib/auth'
import { NextRequest, NextResponse } from 'next/server'

export async function middleware(request: NextRequest) {
  const session = await auth.api.getSession({
    headers: request.headers
  })
  
  const isProtected = request.nextUrl.pathname.startsWith('/dashboard')
  
  if (isProtected && !session) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  
  return NextResponse.next()
}

This middleware runs before any page loads. It checks for sessions efficiently without hitting your database unnecessarily. Better Auth handles the token validation internally.

The real test comes when you need to customize behavior. Suppose we want to log every login attempt. We can extend the auth configuration with hooks:

export const auth = betterAuth({
  // ... previous config
  hooks: {
    onSignIn: async ({ user }) => {
      console.log(`User ${user.email} signed in at ${new Date().toISOString()}`)
      // Here you could add database logging
    }
  }
})

These hooks give you visibility into the authentication flow without having to modify library internals. You get the benefits of a robust system with the flexibility to add your logic where needed.

After implementing this across several projects, I’ve found something interesting. Developers spend less time debugging auth issues and more time building features. The type safety acts as a guard rail, catching mistakes during development instead of in production.

What surprised me most was how this approach changed my testing strategy. Because each piece has clear boundaries and types, I can write focused tests for authentication logic without mocking half the internet.

The setup might seem like more initial work than grabbing a quick authentication snippet. But consider this: How much time have you spent fixing authentication bugs that slipped into production? That initial investment pays dividends every time you add a new feature or onboard another developer to the project.

The goal isn’t just authentication that works. It’s authentication you can trust. When your foundation is solid, everything you build on top becomes more reliable.

I’d love to hear about your experiences with authentication challenges. What pain points have you encountered in your projects? Share your thoughts in the comments below, and if this approach resonates with you, consider sharing it with other developers who might be facing similar struggles.


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: Next.js authentication, Better Auth, Drizzle ORM, TypeScript auth, secure login



Similar Posts
Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Development

Learn how to integrate Next.js with Prisma ORM for type-safe, full-stack web applications. Build database-driven apps with end-to-end TypeScript support.

Blog Image
How to Build Type-Safe, Scalable Apps with Next.js and Prisma

Discover how combining Next.js and Prisma simplifies full-stack development with type safety, clean APIs, and faster workflows.

Blog Image
Complete Guide to Next.js Prisma Integration: Build Type-Safe Full-Stack Applications in 2024

Learn how to integrate Next.js with Prisma ORM for type-safe full-stack apps. Build seamless database operations with auto-generated schemas and TypeScript support.

Blog Image
Build Production-Ready GraphQL API: NestJS, Prisma, PostgreSQL Complete Development Guide

Learn to build a production-ready GraphQL API with NestJS, Prisma, and PostgreSQL. Complete guide with authentication, real-time features, testing, and deployment.

Blog Image
Next.js Prisma Integration Guide: Build Type-Safe Full-Stack Applications with TypeScript

Learn how to integrate Next.js with Prisma ORM for powerful full-stack TypeScript applications. Get end-to-end type safety and seamless database integration.

Blog Image
High-Performance GraphQL APIs: Apollo Server 4, DataLoader, and Redis Caching Complete Guide

Learn to build high-performance GraphQL APIs with Apollo Server 4, DataLoader batching, and Redis caching. Master N+1 query optimization and production deployment.