# Next.js Full-Stack Auth: Better-Auth + Drizzle ORM Guide

* * *

### 🛠️ Pre-Flight Configuration

*   **Tech Stack:** TypeScript, Next.js, PostgresDB
    
*   **Prerequisites:** Basic understanding of next.js
    
*   **GitHub Repository:** [link](https://github.com/OnkarK0273/next-better-auth-template)
    
*   **Live Link** \- [link](https://next-js-better-auth-template.vercel.app/)
    

* * *

In this guide, we will implement production-ready authentication in Next.js using Better-Auth, Drizzle ORM, and PostgreSQL.

![](https://cdn.hashnode.com/uploads/covers/662e9149ea7b8adaf16495b0/6d5df390-7dbf-4dbc-85b7-95b4aa4c3136.png align="center")

# Project setup

## Installation

```bash
# Next.js
pnpm create next-app@latest next-better-template

# Better-auth
pnpm add better-auth

# Drizzle
pnpm add drizzle-orm pg
pnpm add -D drizzle-kit tsx @types/pg
```

## PostgresDB installation

Before configuring Better-Auth, you need an active PostgreSQL instance. We will spin up a local container using Docker Compose, though you can also use managed services like Neon or Supabase.

`docker-compose.yaml`

```yaml
# docker-compose.yaml
services:
  db:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
```

Run following command install images and run pg container on local port `5432`

```bash
docker compose up -d
```

## Configuration

**Set Environment Variables** `.env`

```bash
# You can also use openssl rand -base64 32 to generate
BETTER_AUTH_SECRET= <secret key>

BETTER_AUTH_URL="http://localhost:3000" # Base URL of your app

DATABASE_URL= "postgres://postgres:postgres@localhost:5432/mydb"
```

### Drizzle

```plaintext
📦 <project root>
 ├ 📂 drizzle
 ├ 📂 src
 │   ├ 📂 db
 │   │  └ 📂 schema
 |	 |		 └📜 auth-schema.ts
 │   └ 📜 index.ts
 ├ 📜 .env
 ├ 📜 drizzle.config.ts
 ├ 📜 package.json
 └ 📜 tsconfig.json
```

#### **Connect Drizzle ORM to the database:**

Create a `index.ts` file in the `src` directory and initialize the connection:

```typescript
// src/db/index.ts
import { drizzle } from 'drizzle-orm/node-postgres';

export const db = drizzle(process.env.DATABASE_URL!);
```

#### **Setup Drizzle config file:**

**Drizzle config** - a configuration file that is used by Drizzle Kit and contains all the information about your database connection, migration folder and schema files.

Create a `drizzle.config.ts` file in the root of your project and add the following content:

```typescript
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: './drizzle',
  schema: './src/db/schema',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});
```

#### Setup migration script

Add following script inside `package.json` file for generate and apply migration.

```tsx
// package.json

"scripts": {
    "migration:generate": "npx drizzle-kit generate --config=drizzle.config.ts",
    "migration:migrate": "npx drizzle-kit migrate --config=drizzle.config.ts"
  }
```

### Better-auth

#### **Create A Better Auth Instance and connect DB:**

Create a file named `auth.ts` inside file `src/lib`

Configure Database using `Drizzle-ORM` using built in adapter

```typescript
// src/lib/auth.ts

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

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
  }),
});
```

#### Create database table

Better Auth includes a CLI tool to help manage the schema required by the library.

*   **Generate**: This command generates an ORM schema or SQL migration file.
    
    ```bash
    pnpm dlx auth@latest generate
    ```
    

> **Important:** The CLI outputs `auth-schema.ts` to your root directory. Move this file to `src/db/schema/` to keep your database models modular and co-located.

*   Provide generated `auth-schema` to drizzleAdapter schema property.
    
    ```typescript
    // src/lib/auth.ts
    import { db } from "@/db";
    import { schema } from "@/db/schema/auth-schema";
    import { betterAuth } from "better-auth";
    import { drizzleAdapter } from "better-auth/adapters/drizzle";
    
    export const auth = betterAuth({
      database: drizzleAdapter(db, {
        provider: "pg",
        // provide auth schema to schema property
        schema: schema,
      }),
    });
    ```
    

#### **Applying changes to the database**

1.  Run the following script to generate sql migration file of `auth-schema.ts` which locate inside `drizzle` folder
    
    ```bash
    pnpm run migration:generate
    ```
    
2.  After creating migration file we migrate sql file to generate tables (user, account, session and verification) inside pg database using following script
    
    ```bash
    pnpm run migration:migrate
    ```
    

## Sign-In Methods

### 1\. Email and password

We have to enable email and password option in `auth.ts` file

```typescript
// src/lib/auth.ts
import { db } from "@/db";
import { schema } from "@/db/schema/auth-schema";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
    schema: schema,
  }),
  // to use email and password
  emailAndPassword: {
    enabled: true,
  },
});
```

#### Route handler

To handle API requests, you need to set up a route handler on your server `src/app/api/auth/[...all]/route.ts`

```typescript
// src/app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth"; // path to your auth file
import { toNextJsHandler } from "better-auth/next-js";

export const { POST, GET } = toNextJsHandler(auth);
```

#### Create client instance

The Better-Auth client provides strongly typed authentication helpers to manage client-side state, sessions, and social logins.

`src/lib/auth-client.ts`

```typescript
// src/lib/auth-client.ts
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
    /** The base URL of the server (optional if you're using the same domain) */
    baseURL: "http://localhost:3000"
})
```

#### Usage

using `authClient` we can use `signIn` and `signUp` methods

```typescript
import { authClient } from "@/lib/auth-client";

// signup
await authClient.signUp.email(
        {
          email: value.email,
          password: value.password,
          name: value.username,
          callbackURL: "/",
        },
        {
          onRequest: (ctx) => {
            //show loading    
          },
          onSuccess: (ctx) => {
            //redirect to the dashboard or sign in page        
          },
          onError: (ctx) => {
            // display the error message     
          },
        },
      );
      
   // signin
   await authClient.signIn.email(
        {
          email: value.email,
          password: value.password,
          callbackURL: "/",
        },
        {
          onRequest: (ctx) => {
            //show loading
          },
          onSuccess: (ctx) => {
            //redirect to the dashboard or sign in page       
          },
          onError: (ctx) => {
            // display the error message  
          },
        },
      );
```

### 2\. Google

Get your google credential on  **Google Cloud Console.**

1: Set consent screen

2: Create **OAuth client ID →** Choose **Web application**

Add your redirect URIs:

*   `http://localhost:3000/api/auth/callback/google` (for local development)
    
*   `https://your-domain.com/api/auth/callback/google` (for production)
    

3: Copy the **Client ID** and **Client Secret** into your environment variables

#### Configure the provider:

To configure the provider, you need to pass the the `clientId` and `clientSecret` with other options in `socialProviders.google` for your auth configuration.

```typescript
// src/lib/auth.ts
import { betterAuth } from "better-auth"

export const auth = betterAuth({
    baseURL: process.env.BETTER_AUTH_URL, 
    socialProviders: {
        google: { 
            clientId: process.env.GOOGLE_CLIENT_ID as string, 
            clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, 
            // to get refresh token
            accessType: "offline",
            // always ask the user to select an account
			      prompt: "select_account consent",
        }, 
    },
})
```

#### Usage:

you can use the `signIn.social` function provided by the client for both signin and signout form. The `signIn` function takes an object with the following properties:

*   `provider`: The provider to use. It should be set to `google`.
    
    ```typescript
    import { createAuthClient } from "better-auth/client";
    const authClient = createAuthClient();
    
    const signIn = async () => {
      const data = await authClient.signIn.social({
        provider: "google",
      });
    };
    ```
    

### 3\. GitHub

#### GitHub credential:

To use GitHub sign in, you need a client ID and client secret. You can get them from the **GitHub Developer Portal**. Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/github` for local development

#### **Configure the provider:**

```typescript
// src/lib/auth.ts
import { betterAuth } from "better-auth"

export const auth = betterAuth({
    socialProviders: {
        github: { 
            clientId: process.env.GITHUB_CLIENT_ID as string, 
            clientSecret: process.env.GITHUB_CLIENT_SECRET as string, 
        }, 
    },
})
```

#### Usage:

you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:

*   `provider`: The provider to use. It should be set to `github`.
    
    ```typescript
    import { createAuthClient } from "better-auth/client";
    const authClient = createAuthClient();
    
    const signIn = async () => {
      const data = await authClient.signIn.social({
        provider: "github",
      });
    };
    ```
    

## Logout Method

you can use the `signOut()` method provide by `authClient` for logout the user.

```typescript
	import { authClient } from "@/lib/auth-client";
  
  const logout = async () => {
    await authClient.signOut({
      fetchOptions: {
        onSuccess: () => {
          // redirect to home page
        },
      },
    });
  };;
```

## How to protect route

### 1\. Public layout

Authenticates incoming requests on protected layouts by checking for an active session; unauthenticated traffic is redirected to `/signin`.

```typescript
// src/app/layout.ts
import { cn } from "@/lib/utils";
import "./globals.css";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function RootLayout({ children }: LayoutProps<"/">) {
  const session = await auth.api.getSession({
    headers: await headers(),
  });

  if (!session) {
    redirect("/signin");
  }

  return (
    <html
      lang="en"
      className={cn(
        "h-full",
        "antialiased",
        "font-sans",
      )}
    >
      <body className="min-h-full flex flex-col">
        {children}
      </body>
    </html>
  );
}

```

### 2\. Auth layout

Prevents authenticated users from re-accessing login or registration pages by redirecting active sessions directly to the dashboard root.

```typescript
// src/app/(auth)/layout.ts
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function AuthLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await auth.api.getSession({
    headers: await headers(),
  })

  if (session) {
    redirect("/");
  }
  return <section>{children}</section>;
}

```

## Reference

Official Document - [Better-Auth](https://better-auth.com/docs/installation), [Drizzle-ORM](https://orm.drizzle.team/docs/get-started/postgresql-new), [Nextjs](https://nextjs.org/docs)
