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
How Next.js 14 Server Actions Simplified My Full-Stack Forms

Discover how Server Actions in Next.js 14 eliminate boilerplate, improve validation, and streamline full-stack form development.

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

Learn how to integrate Next.js with Prisma ORM for type-safe full-stack development. Build powerful React apps with seamless database connectivity and auto-generated APIs.

Blog Image
Build Distributed Rate Limiter with Redis, Node.js, and TypeScript: Production-Ready Guide

Build distributed rate limiter with Redis, Node.js & TypeScript. Learn token bucket, sliding window algorithms, Express middleware, failover handling & production deployment strategies.

Blog Image
How to Build Real-Time Web Apps with Svelte and Supabase Integration in 2024

Learn to build real-time web apps with Svelte and Supabase integration. Discover seamless database operations, authentication, and live updates for modern development.

Blog Image
Build Full-Stack Apps: Complete Next.js and Prisma Integration Guide for Modern Developers

Learn how to integrate Next.js with Prisma for powerful full-stack development. Build type-safe applications with unified frontend and backend code.

Blog Image
Build Type-Safe Event-Driven Microservices with TypeScript NestJS and Apache Kafka Complete Guide

Learn to build scalable TypeScript microservices with NestJS and Apache Kafka. Master event-driven architecture, type-safe schemas, and production deployment patterns.