> Full Neon documentation index: https://neon.com/docs/llms.txt

# Getting started with Managed Better Auth and Next.js

Learn how to set up Managed Better Auth in a Next.js application

This guide walks you through building a demo todo application with **Next.js**, [Managed Better Auth](https://neon.com/docs/auth/overview), and **Drizzle ORM**. By following along, you'll learn how to integrate Managed Better Auth into your Next.js projects and manage database interactions with Drizzle ORM.

The guide focuses on using **Server Actions** to handle authentication and database operations on the server. [Optional steps](https://neon.com/guides/neon-auth-nextjs#optional-accessing-user-data-elsewhere) at the end show other ways of retrieving user information in a Next.js app (e.g., server components, client components, API routes).

By the end, you'll have a todo application where users can sign up, log in, and manage their todos.

## Prerequisites

Before you begin, ensure you have the following:

- **Node.js:** Version `18` or later installed on your machine. You can download it from [nodejs.org](https://nodejs.org/).
- **Neon account:** A Neon account (the Free plan works). If you don't have one, sign up at [Neon](https://console.neon.tech/signup).

## Create a Neon project with Managed Better Auth

You'll need to create a Neon project and enable Managed Better Auth.

1. **Create a Neon project:** Navigate to the [Neon Console](https://console.neon.tech) to create a new Neon project. Give your project a name, such as `next-neon-todo`.

2. **Enable Auth:**
   - In your project's dashboard, go to the **Auth** page.
   - Click **Enable Auth** to set up authentication for your project.

3. **Copy your credentials:**
   - **Auth URL:** Found on the **Auth** page under Configuration (e.g., `https://ep-xxx.neonauth.us-east-1.aws.neon.tech/neondb/auth`).
     ![Managed Better Auth URL](https://neon.com/docs/auth/neon-auth-base-url.png)
   - **Database connection string:** Click **Connect** on your project dashboard and enable the **Connection pooling** toggle to get the pooled connection string.
     ![Connection modal](https://neon.com/docs/connect/connect_to_branch_modal.png)

## Set up the Next.js project

Create a new Next.js project and install dependencies.

1. **Initialize the app:**

   ```bash
   npx create-next-app@latest next-neon-todo --yes
   cd next-neon-todo
   ```

2. **Install dependencies:**

   ```bash
   npm install @neondatabase/auth@latest @neondatabase/auth-ui @neondatabase/serverless drizzle-orm
   npm install -D drizzle-kit dotenv @types/node
   ```

## Configure environment variables

Create a `.env` file in the root of your project.

**Note:** Replace the Auth URL with your actual Auth URL from the Neon Console. Generate a secure cookie secret with `openssl rand -base64 32`.

```env
DATABASE_URL="postgresql://alex:AbC123dEf@ep-cool-darkness-a1b2c3d4-pooler.us-east-2.aws.neon.tech/dbname?sslmode=require&channel_binding=require"
NEON_AUTH_BASE_URL="https://ep-xxx.neonauth.us-east-1.aws.neon.tech/neondb/auth"
NEON_AUTH_COOKIE_SECRET="your-secret-at-least-32-characters-long"
```

## Set up Drizzle ORM

Drizzle ORM manages your database schema and queries. You can use any Postgres client instead.

Every database operation filters data by the authenticated user that Managed Better Auth provides.

### Create Drizzle config

Create `drizzle.config.ts` in the root of your project:

```typescript
import 'dotenv/config';
import type { Config } from 'drizzle-kit';

export default {
  schema: './app/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  schemaFilter: ['public', 'neon_auth'],
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
} satisfies Config;
```

This config tells Drizzle Kit where to find your database schema and where to output migration files. The `schemaFilter` is configured to look at both the `public` and `neon_auth` schemas. The `neon_auth` schema is where Managed Better Auth stores its user data.

### Pull Managed Better Auth schema

Managed Better Auth creates and maintains the Better Auth tables in the `neon_auth` schema. Since these tables live in your Neon database, you can work with them directly using SQL queries or any Postgres‑compatible ORM, including defining foreign key relationships.

To integrate Managed Better Auth tables into your Drizzle ORM setup, you need to introspect the existing `neon_auth` schema and generate the corresponding Drizzle schema definitions.

This step makes Drizzle aware of the Managed Better Auth tables, so you can create relationships between your application data (like the `todos` table) and the user data managed by Managed Better Auth.

1. **Introspect the database:**
   Run the Drizzle Kit `pull` command to generate a schema file based on your existing Neon database tables.

   ```bash
   npx drizzle-kit pull
   ```

   This command connects to your Neon database, inspects its structure, and creates `schema.ts` and `relations.ts` files inside a new `drizzle` folder. These files contain the Drizzle schema definitions for the Managed Better Auth tables.

2. **Organize schema files:**
   Create a new directory `app/db`. Move the generated `schema.ts` and `relations.ts` files from the `drizzle` directory to `app/db/schema.ts` and `app/db/relations.ts` respectively.

   ```
    ├ 📂 drizzle
    │ ├ 📂 meta
    │ ├ 📜 migration.sql
    │ ├ 📜 relations.ts ────────┐
    │ └ 📜 schema.ts ───────────┤
    ├ 📂 app                    │
    │ ├ 📂 db                   │
    │ │ ├ 📜 relations.ts <─────┤
    │ │ └ 📜 schema.ts <────────┘
    │ └ 📜 App.tsx
    └ …
   ```

3. **Add the todos table to your schema**

   Open `app/db/schema.ts` to view the `neon_auth` tables that Drizzle generated from your existing Neon database schema. At the bottom of the file, append the `todos` table definition as shown below:

   ```typescript {9,39-49}
   import {
     pgTable,
     pgSchema,
     uuid,
     text,
     timestamp,
     unique,
     boolean,
     bigint,
   } from 'drizzle-orm/pg-core';
   import { sql } from 'drizzle-orm';

   export const neonAuth = pgSchema('neon_auth');

   // .. other Managed Better Auth table definitions ..

   export const userInNeonAuth = neonAuth.table(
     'user',
     {
       id: uuid().defaultRandom().primaryKey().notNull(),
       name: text().notNull(),
       email: text().notNull(),
       emailVerified: boolean().notNull(),
       image: text(),
       createdAt: timestamp({ withTimezone: true, mode: 'string' })
         .default(sql`CURRENT_TIMESTAMP`)
         .notNull(),
       updatedAt: timestamp({ withTimezone: true, mode: 'string' })
         .default(sql`CURRENT_TIMESTAMP`)
         .notNull(),
       role: text(),
       banned: boolean(),
       banReason: text(),
       banExpires: timestamp({ withTimezone: true, mode: 'string' }),
     },
     (table) => [unique('user_email_key').on(table.email)]
   );

   export const todos = pgTable('todos', {
     id: bigint('id', { mode: 'number' }).primaryKey().generatedByDefaultAsIdentity(),
     text: text('text').notNull(),
     completed: boolean('completed').notNull().default(false),
     userId: uuid('user_id')
       .notNull()
       .references(() => userInNeonAuth.id),
     createdAt: timestamp('created_at').defaultNow(),
   });

   export type Todo = typeof todos.$inferSelect;
   ```

   The `todos` table contains the following columns: `id`, `text`, `completed`, and `user_id`. It is linked to the `user` table in the `neon_auth` schema via a foreign key relationship on the `user_id` column.

### Generate and apply migrations

Now, generate the SQL migration file to create the `todos` table.

```bash
npx drizzle-kit generate
```

This creates a new SQL file in the `drizzle` directory. Apply this migration to your Neon database by running:

**Important: Issue with commented migrations**

This is a [known issue](https://github.com/drizzle-team/drizzle-orm/issues/4851) in Drizzle. If `drizzle-kit pull` generated an initial migration file (e.g., `0000_...sql`) wrapped in block comments (`/* ... */`), `drizzle-kit migrate` may fail with an `unterminated /* comment` error.

To resolve this, manually delete the contents of the `0000_...sql` file or replace the block comments with line comments (`--`).

```bash
npx drizzle-kit migrate
```

Your `todos` table now exists in your Neon database. You can verify this in the **Tables** section of your Neon project dashboard.

### Initialize database client

Create `app/db/index.ts` to initialize the Drizzle ORM client.

```typescript
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';

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

Now you have Drizzle ORM set up with Managed Better Auth and a `todos` table ready for use in your Next.js application.

## Set up Managed Better Auth

Integrate Managed Better Auth into your Next.js application for authentication and session management.

### Create auth server instance

Create a file `lib/auth/server.ts` at the root of your project. This single instance provides all server-side auth functionality: `.handler()` for API routes, `.middleware()` for route protection, and `.getSession()` for accessing session data.

```typescript
import { createNeonAuth } from '@neondatabase/auth/next/server';

export const auth = createNeonAuth({
  baseUrl: process.env.NEON_AUTH_BASE_URL!,
  cookies: {
    secret: process.env.NEON_AUTH_COOKIE_SECRET!,
  },
});
```

### Create auth client

Create a file `lib/auth/client.ts` at the root of your project to initialize the Managed Better Auth client for browser-side auth operations.

```typescript
'use client';
import { createAuthClient } from '@neondatabase/auth/next';

export const authClient = createAuthClient();
```

### Create API route

Create `app/api/auth/[...path]/route.ts`. This file will handle authentication API requests on the server side.

```typescript
import { auth } from '@/lib/auth/server';

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

### Add Managed Better Auth UI provider

Update `app/layout.tsx` to wrap your application with the `NeonAuthUIProvider`, which supplies authentication context and UI components.

This setup also adds a global header, visible on every page, containing a `UserButton` from [Managed Better Auth UI components](https://neon.com/docs/auth/reference/ui-components) for account management.

```tsx
import { authClient } from '@/lib/auth/client';
import { NeonAuthUIProvider, UserButton } from '@neondatabase/auth-ui';
import './globals.css';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <NeonAuthUIProvider authClient={authClient} emailOTP social={{ providers: ['google'] }}>
          <header className="flex h-16 items-center justify-between border-b p-4">
            <h1 className="text-xl font-bold">Next.js Neon Todo</h1>
            <UserButton size={'icon'} />
          </header>
          {children}
        </NeonAuthUIProvider>
      </body>
    </html>
  );
}
```

### Add Managed Better Auth styles

In `app/globals.css`, add the following import statement directly below the `@import 'tailwindcss';` line.

This includes the Tailwind styles that Managed Better Auth UI components need.

```css {2}
@import 'tailwindcss';
@import '@neondatabase/auth-ui/tailwind';

/* ... your existing styles ... */
```

### Create auth pages

Create the pages for signing in and managing accounts using the prebuilt Managed Better Auth UI components.

1. **Auth page:**

   Create `app/auth/[path]/page.tsx`. This page will render the Managed Better Auth sign-in/sign-up UI.

   ```tsx
   import { AuthView } from '@neondatabase/auth-ui';

   export const dynamicParams = false;

   export default async function AuthPage({ params }: { params: Promise<{ path: string }> }) {
     const { path } = await params;

     return (
       <main className="container mx-auto flex grow flex-col items-center justify-center gap-3 self-center p-4 md:p-6">
         <AuthView path={path} />
       </main>
     );
   }
   ```

2. **Account page:**

   Create `app/account/[path]/page.tsx`. This page renders the Managed Better Auth account management UI, including features such as profile settings, password updates, and more.

   ```tsx
   import { AccountView } from '@neondatabase/auth-ui';
   import { accountViewPaths } from '@neondatabase/auth-ui/server';

   export const dynamicParams = false;

   export function generateStaticParams() {
     return Object.values(accountViewPaths).map((path) => ({ path }));
   }

   export default async function AccountPage({ params }: { params: Promise<{ path: string }> }) {
     const { path } = await params;

     return (
       <main className="container p-4 md:p-6">
         <AccountView path={path} />
       </main>
     );
   }
   ```

## Create server actions

You'll use Server Actions to run database operations on the server. These actions make sure only authenticated users can access and modify their todos.

Create `app/actions.ts` with the following content:

```typescript
'use server';

import { auth } from '@/lib/auth/server';
import { db } from '@/app/db';
import { todos } from '@/app/db/schema';
import { eq, desc, and } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';

async function getAuthUser() {
  const { data: session } = await auth.getSession();
  if (!session?.user) throw new Error('Unauthorized');
  return session.user;
}

export async function getTodos() {
  const user = await getAuthUser();
  return db.select().from(todos).where(eq(todos.userId, user.id)).orderBy(desc(todos.createdAt));
}

export async function addTodo(formData: FormData) {
  const user = await getAuthUser();
  const text = formData.get('text') as string;
  if (!text) return;

  await db.insert(todos).values({ text, userId: user.id });

  revalidatePath('/');
}

export async function toggleTodo(id: number, currentStatus: boolean) {
  const user = await getAuthUser();

  await db
    .update(todos)
    .set({ completed: !currentStatus })
    .where(and(eq(todos.id, id), eq(todos.userId, user.id)));

  revalidatePath('/');
}

export async function deleteTodo(id: number) {
  const user = await getAuthUser();

  await db.delete(todos).where(and(eq(todos.id, id), eq(todos.userId, user.id)));

  revalidatePath('/');
}
```

The file defines five Server Actions that handle authentication and database operations:

1. `getAuthUser()`
   - Calls `auth.getSession()` to get the current user.
   - Throws an error if no user is authenticated.
   - Used internally by all other actions to enforce authentication.

2. `getTodos()`
   - Retrieves todos belonging to the authenticated user.
   - Filters by `userId` and orders results by `createdAt` in descending order.
   - Ensures users only see their own todos.

3. `addTodo(formData)`
   - Extracts the `text` field from submitted form data.
   - Inserts a new todo linked to the authenticated user.
   - Calls `revalidatePath('/')` to refresh the UI after insertion.

4. `toggleTodo(id, currentStatus)`
   - Flips the `completed` status of a todo.
   - Ensures the update only applies to the authenticated user's todo (via `userId` check).
   - Revalidates the path to update the UI.

5. `deleteTodo(id)`
   - Deletes a todo by its ID.
   - Restricts deletion to the authenticated user's own todos.
   - Revalidates the path to reflect changes in the UI.

## Create middleware for protected routes

To restrict certain routes to authenticated users, create a middleware file. In this case, you'll protect the main page (`/`).

Create `proxy.ts` in the root of your project with the following content:

**Note: Next.js version compatibility**

`proxy.ts` replaces `middleware.ts` in Next.js 16. On earlier versions, name the file `middleware.ts` and export `default function middleware` instead of `proxy`. The auth logic is identical.

```typescript
import { auth } from '@/lib/auth/server';
import { NextRequest } from 'next/server';

const authMiddleware = auth.middleware({
  loginUrl: '/auth/sign-in',
});

export default function middleware(request: NextRequest) {
  if (request.headers.has('Next-Action')) {
    return;
  }
  return authMiddleware(request);
}

export const config = {
  matcher: ['/', '/account/:path*'],
};

```

The middleware uses `auth.middleware()` to check if a user is authenticated when accessing protected routes (`/` and `/account/:path*`). If not authenticated, the user is redirected to the sign-in page (`/auth/sign-in`). Requests with a `Next-Action` header (server actions) are allowed through without the auth check, since authentication is handled within the server actions themselves.

## Create frontend components

Create the main page and components to display and manage todos.

1. **Todo item component:**

   Create `app/components/TodoItem.tsx` with the following content:

   ```tsx
   'use client';
   import { toggleTodo, deleteTodo } from '../actions';
   import { Todo } from '@/app/db/schema';

   export function TodoItem({ todo }: { todo: Todo }) {
     return (
       <li className="bg-gray-50 dark:bg-gray-800 mb-2 flex items-center justify-between rounded p-3">
         <div
           className="flex cursor-pointer items-center gap-2"
           onClick={() => toggleTodo(todo.id, todo.completed)}
         >
           <input type="checkbox" checked={todo.completed} readOnly className="cursor-pointer" />
           <span className={todo.completed ? 'text-gray-500 line-through' : ''}>{todo.text}</span>
         </div>
         <button onClick={() => deleteTodo(todo.id)} className="text-red-400 hover:text-red-600">
           Delete
         </button>
       </li>
     );
   }
   ```

   The TodoItem component displays an individual todo item with a checkbox to toggle its completion status and a delete button. It uses the `toggleTodo` and `deleteTodo` server actions to perform these operations.

2. **Main page:**

   Update `app/page.tsx` with the following content:

   ```tsx
   import { getTodos, addTodo } from '@/app/actions';
   import { TodoItem } from '@/app/components/TodoItem';

   export default async function Home() {
     const todos = await getTodos();

     return (
       <main className="dark:bg-gray-900 mx-auto mt-10 max-w-md rounded-lg bg-white p-6 shadow">
         <h2 className="mb-6 text-2xl font-bold">My Tasks</h2>

         <form action={addTodo} className="mb-6 flex gap-2">
           <input
             name="text"
             type="text"
             placeholder="Add a new task..."
             className="flex-1 rounded border p-2"
             required
           />
           <button
             type="submit"
             className="bg-blue-600 hover:bg-blue-700 rounded px-4 py-2 text-white"
           >
             Add
           </button>
         </form>

         <ul>
           {todos.map((todo) => (
             <TodoItem key={todo.id} todo={todo} />
           ))}
           {todos.length === 0 && <p className="text-gray-500 text-center">No tasks yet.</p>}
         </ul>
       </main>
     );
   }
   ```

   This page fetches the authenticated user's todos using the `getTodos` server action and displays them.
   Since it is protected by the middleware, only logged-in users can access it. It also includes a form to add new todos using the `addTodo` server action.

## Run the application

1. Start the development server:

   ```bash
   npm run dev
   ```

2. Open `http://localhost:3000`.

3. You'll be redirected to the sign-in page.

4. Once logged in, you can manage your todos.

   ![Todo App Screenshot](https://neon.com/docs/guides/neon-auth-nextjs-todo-demo.png)

## Optional: Accessing user data elsewhere

This guide used **Server Actions** to handle data, but your application might need to access the user's session in other contexts, such as rendering a user profile on the server, reacting to session changes on the client, or securing a REST API endpoint.

Here is how you can retrieve user information across different parts of the Next.js stack:

**Server components**

**Server components (RSC)**

In Server components, you can access session data using the `auth` instance to retrieve the current `session` and `user` objects. Use this for initial page loads and conditional rendering based on auth state.

Create `app/server-profile/page.tsx`:

```tsx
import { auth } from '@/lib/auth/server';

export const dynamic = 'force-dynamic';

export default async function ServerProfilePage() {
  const { data: session } = await auth.getSession();

  return (
    <div className="mx-auto max-w-xl space-y-4 p-6">
      <h1 className="text-2xl font-bold">Server-Side Profile</h1>

      <div className="bg-gray-100 dark:bg-gray-800 rounded p-4">
        <p>
          <strong>Status:</strong> {session ? '✅ Authenticated' : '❌ Guest'}
        </p>
        {session?.user && (
          <p>
            <strong>User ID:</strong> {session.user.id}
          </p>
        )}
        {session?.user && (
          <p>
            <strong>Email:</strong> {session.user.email}
          </p>
        )}
      </div>

      <pre className="overflow-auto rounded bg-black p-4 text-xs text-white">
        {JSON.stringify({ session: session?.session, user: session?.user }, null, 2)}
      </pre>
    </div>
  );
}
```

**Client components**

**Client components**

For interactive components that need to react to authentication state changes (like showing a loading spinner while checking a session), use the `authClient.useSession()` hook.

Create `app/client-profile/page.tsx`:

```tsx
'use client';

import { authClient } from '@/lib/auth/client';

export default function ClientProfilePage() {
  // The hook automatically updates if the session changes
  const { data, isPending, error } = authClient.useSession();

  if (isPending) return <div className="p-6">Loading session...</div>;

  return (
    <div className="mx-auto max-w-xl space-y-4 p-6">
      <h1 className="text-2xl font-bold">Client-Side Profile</h1>

      <div className="bg-gray-100 dark:bg-gray-800 rounded p-4">
        <p>
          <strong>Status:</strong> {data?.session ? '✅ Authenticated' : '❌ Guest'}
        </p>
        {data?.user && (
          <p>
            <strong>User ID:</strong> {data.user.id}
          </p>
        )}
      </div>

      <pre className="overflow-auto rounded bg-black p-4 text-xs text-white">
        {JSON.stringify(data, null, 2)}
      </pre>
    </div>
  );
}
```

**API Routes**

**API routes (route handlers)**

You can also secure API routes by validating the session using `auth.getSession()` within your route handlers. This is useful for building RESTful endpoints that require authentication.

Create `app/api/profile/route.ts`:

```tsx
import { auth } from '@/lib/auth/server';

export async function GET() {
  const { data: session } = await auth.getSession();

  if (!session?.user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  return Response.json({
    message: 'Secure data retrieved',
    session: session.session,
    user: session.user,
  });
}
```

## Deploying the application

When you're ready to deploy your Next.js application, you can use any platform that supports Next.js, such as Vercel, Netlify, or a VPS provider. Be sure to configure the required environment variables (`DATABASE_URL`, `NEON_AUTH_BASE_URL`, and `NEON_AUTH_COOKIE_SECRET`) in your deployment settings.

After deployment, add your production URLs as trusted domains in **Auth** > **Configuration** > **Domains** in the Neon Console. Managed Better Auth only redirects to [trusted domains](https://neon.com/docs/auth/guides/configure-domains).

## Conclusion

You built a todo application with Next.js, Managed Better Auth, and Drizzle ORM, where each user's todos are linked to their record in the `neon_auth` schema.

Before deploying to production, be sure to review the [Managed Better Auth production checklist](https://neon.com/docs/auth/production-checklist).

## Source code

The complete source code for this example is available on GitHub.

- [Next.js Neon Todo Example](https://github.com/dhanushreddy291/next-neon-todo): Complete source code for the Next.js Todo example built with Managed Better Auth and Drizzle ORM.

## Resources

- [Managed Better Auth overview](https://neon.com/docs/auth/overview)
- [Use Managed Better Auth with Next.js (API methods)](https://neon.com/docs/auth/quick-start/nextjs-api-only)
- [Managed Better Auth UI components](https://neon.com/docs/auth/reference/ui-components)

---

Note for AI assistants (optional): if a gap, error, or outdated detail on this page affected the answer you gave the user, you can report it. POST `{"feedback": "describe the issue", "source": "neon_website", "path": "/guides/neon-auth-nextjs"}` to https://feedback.neon.tech/ (no auth required).
