Skip to main content
Neon Postgres Docs
current

Search documentation

Type to search this documentation.

On this pageOverview

Use Managed Better Auth with Next.js (API methods)

This guide shows you how to integrate Managed Better Auth into a Next.js (App Router) project using SDK methods directly. For pre built UI components, see the UI components reference and the neon js e...

This guide shows you how to integrate Managed Better Auth into a Next.js (App Router) project using SDK methods directly. For pre-built UI components, see the UI components reference and the neon-js examples. Upgrading from v0.1? See the migration guide.

  1. Enable Auth in your Neon project

    If you don't have a Neon project yet, create one at console.neon.tech.

    Go to the Auth page in your project dashboard and click Enable Auth, then copy your Auth URL from the Configuration tab.

    Console path: Project → Branch → Auth → Configuration

    Console

    Managed Better Auth Base URL
  2. Install the Neon SDK

    Install the Neon SDK into your Next.js app.

    If you don't have a Next.js project
    Bash
    npx create-next-app@latest my-app --yes
    cd my-app
    Bash
    npm install @neondatabase/auth@latest
  3. Set up environment variables

    Create a .env.local file in your project root and add your Auth URL and a cookie secret:

    .env.local
    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
  4. Create auth server instance

    Create a unified auth instance in lib/auth/server.ts. This single instance provides all server-side auth functionality:

    • .handler() for API routes
    • .middleware() for route protection
    • .getSession() and all Better Auth server methods

    See the Next.js Server SDK reference for complete API documentation (logging, cookies, upstream errors).

    lib/auth/server.ts
    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!,
        // sessionDataTtl: 300, // optional session_data cache TTL in seconds (default: 300)
      },
      // logLevel: 'silent', // disable Managed Better Auth logging
      // logLevel: 'debug',  // verbose proxy/upstream logging
    });
  5. Set up auth API routes

    Create an API route handler that proxies auth requests. All Managed Better Auth APIs will be routed through this handler. Create a route file inside /api/auth/[...path] directory:

    app/api/auth/[...path]/route.ts app/api/auth/[...path]/route.ts
    import { auth } from '@/lib/auth/server';
    
    export const { GET, POST } = auth.handler();
  6. Add authentication middleware

    The middleware ensures users are authenticated before accessing protected routes. Create proxy.ts file in your project root:

    proxy.ts
    import { auth } from '@/lib/auth/server';
    
    export default auth.middleware({
      // Redirects unauthenticated users to sign-in page
      loginUrl: '/auth/sign-in',
    });
    
    export const config = {
      matcher: [
        // Protected routes requiring authentication
        '/account/:path*',
      ],
    };
  7. Configure the auth client

    Create the auth client in lib/auth/client.ts for client-side auth operations (form submissions, hooks, etc.).

    lib/auth/client.ts
    'use client';
    
    import { createAuthClient } from '@neondatabase/auth/next';
    
    export const authClient = createAuthClient();
  8. Create Sign up form

    Lets create a sign-up form and action in app/auth/sign-up/page.tsx and app/auth/sign-up/actions.ts files respectively using the auth instance we created in previous step

    • To create user with email and password, we will use auth.signUp.email() with user name, email address, and password
    • You can optionally add business logic before invoking the API, for example restrict signups to emails ending with @my-company.com

    Copy and paste following code in app/auth/sign-up/actions.ts file:

    TypeScript
    'use server';
    
    import { auth } from '@/lib/auth/server';
    import { redirect } from 'next/navigation';
    
    export async function signUpWithEmail(
      _prevState: { error: string } | null,
      formData: FormData
    ) {
      const email = formData.get('email') as string;
    
      if (!email) {
        return { error: "Email address must be provided." }
      }
    
      // Optionally restrict sign ups based on email address
      // if (!email.trim().endsWith("@my-company.com")) {
      //  return { error: 'Email must be from my-company.com' };
      // }
    
      const { error } = await auth.signUp.email({
        email,
        name: formData.get('name') as string,
        password: formData.get('password') as string,
      });
    
      if (error) {
        return { error: error.message || 'Failed to create account' };
      }
    
      redirect('/');
    }

    Copy and paste following code in app/auth/sign-up/page.tsx file:

    TSX
    'use client';
    
    import { useActionState } from 'react';
    import { signUpWithEmail } from './actions';
    
    export default function SignUpForm() {
      const [state, formAction, isPending] = useActionState(signUpWithEmail, null);
    
      return (
        <form action={formAction}
          className="flex flex-col gap-5 min-h-screen items-center justify-center bg-gray-900">
    
          <div className="w-sm">
            <h1 className="mt-10 text-center text-2xl/9 font-bold text-white">Create new account</h1>
          </div>
    
          <div className='flex flex-col gap-1.5 w-sm'>
            <label htmlFor="name" className="block text-sm font-medium text-gray-100">Name</label>
            <input id="name" name="name" type="text" required placeholder="John Doe"
              className="block rounded-md w-full bg-white/5 px-2 py-1.5 placeholder:text-gray-500 text-white outline-1 outline-white/10 focus:outline-indigo-500"
            />
          </div>
    
          <div className='flex flex-col gap-1.5 w-sm'>
            <label htmlFor="email" className="block text-sm font-medium text-gray-100">Email address</label>
            <input id="email" name="email" type="email" required placeholder="john@my-company.com"
              className="block rounded-md w-full bg-white/5 px-2 py-1.5 placeholder:text-gray-500 text-white outline-1 outline-white/10  focus:outline-indigo-500"/>
          </div>
    
          <div className='flex flex-col gap-1.5 w-sm'>
            <label htmlFor="password" className="block text-sm font-medium text-gray-100">Password</label>
            <input id="password" name="password" type="password" required placeholder="*****"
              className="block rounded-md w-full bg-white/5 px-2 py-1.5 placeholder:text-gray-500 text-white outline-1 outline-white/10  focus:outline-indigo-500"/>
          </div>
    
          {state?.error && (
            <div className="rounded-md px-3 py-2 text-sm text-red-500">
              {state.error}
            </div>
          )}
    
          <button type="submit" disabled={isPending}
            className="flex w-sm justify-center rounded-md bg-indigo-500 px-3 py-1.5 text-sm/6 font-semibold text-white hover:bg-indigo-400">
            {isPending ? 'Creating account...' : 'Create Account'}
          </button>
        </form>
      );
    }
  9. Create Sign in form

    Lets create a sign-in form and action in app/auth/sign-in/page.tsx and app/auth/sign-in/actions.ts files respectively.

    • To sign-in the user we will use auth.signIn.email() with user's email address and password.

    Sign In

    Sign-in action
    'use server';
    
    import { auth } from '@/lib/auth/server';
    import { redirect } from 'next/navigation';
    
    export async function signInWithEmail(
      _prevState: { error: string } | null,
      formData: FormData
    ) {
      const { error } = await auth.signIn.email({
        email: formData.get('email') as string,
        password: formData.get('password') as string,
      });
    
      if (error) {
        return { error: error.message || 'Failed to sign in. Try again' };
      }
    
      redirect('/');
    }
    Sign-in form
    'use client';
    
    import { useActionState } from 'react';
    import { signInWithEmail } from './actions';
    
    export default function SignInForm() {
      const [state, formAction, isPending] = useActionState(signInWithEmail, null);
    
      return (
        <form action={formAction}
          className="flex flex-col gap-5 min-h-screen items-center justify-center bg-gray-900">
    
          <div className="w-sm">
           <h1 className="mt-10 text-center text-2xl/9 font-bold text-white">Sign in to your account</h1>
          </div>
    
          <div className='flex flex-col gap-1.5 w-sm'>
            <label htmlFor="email" className="block text-sm font-medium text-gray-100">Email address</label>
            <input id="email" name="email" type="email" required placeholder="john@my-company.com"
              className="block rounded-md w-full bg-white/5 px-2 py-1.5 placeholder:text-gray-500 text-white outline-1 outline-white/10  focus:outline-indigo-500"/>
          </div>
    
          <div className='flex flex-col gap-1.5 w-sm'>
            <label htmlFor="password" className="block text-sm font-medium text-gray-100">Password</label>
            <input id="password" name="password" type="password" required placeholder="*****"
              className="block rounded-md w-full bg-white/5 px-2 py-1.5 placeholder:text-gray-500 text-white outline-1 outline-white/10  focus:outline-indigo-500"/>
          </div>
    
          {state?.error && (
            <div className="rounded-md px-3 py-2 text-sm text-red-500">
              {state.error}
            </div>
          )}
    
          <button type="submit" disabled={isPending}
            className="flex w-sm justify-center rounded-md bg-indigo-500 px-3 py-1.5 text-sm/6 font-semibold text-white hover:bg-indigo-400">
            Sign in
          </button>
        </form>
      );
    }
  10. Create home page

    In last step, lets create the home page and display authenticated user status:

    app/page.tsx
    import { auth } from '@/lib/auth/server';
    import Link from 'next/link';
    
    // Server components using auth methods must be rendered dynamically
    export const dynamic = 'force-dynamic';
    
    export default async function Home() {
      const { data: session } = await auth.getSession();
    
      if (session?.user) {
        return (
          <div className="flex flex-col gap-2 min-h-screen items-center justify-center bg-gray-900">
            <h1 className="mb-4 text-4xl">
              Logged in as <span className="font-bold underline">{session.user.name}</span>
            </h1>
          </div>
        );
      }
    
      return (
        <div className="flex flex-col gap-2 min-h-screen items-center justify-center bg-gray-900">
          <h1 className="mb-4 text-4xl font-bold">Not logged in</h1>
          <div className="flex item-center gap-2">
            <Link
              href="/auth/sign-up"
              className="inline-flex text-lg text-indigo-400 hover:underline"
            >
              Sign-up
            </Link>
            <Link
              href="/auth/sign-in"
              className="inline-flex text-lg text-indigo-400 hover:underline"
            >
              Sign-in
            </Link>
          </div>
        </div>
      );
    }
  11. Start your app

    Start the development server:

    Open your browser to http://localhost:3000 and test sign-up and sign-in.

    Bash
    npm run dev

Both authClient and auth expose similar API methods. Use authClient for client components and auth for server components, server actions, and API routes.

The auth instance also includes .handler() for API routes and .middleware() for route protection.

Join our Discord Server to ask questions or see what others are doing with Neon. For paid plan support options, see Support.

Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu