# How to Set Up Supabase Auth with Next.js App Router

- Tool: Supabase
- Difficulty: Intermediate
- Time required: 20-25 min
- Compatibility: Supabase (all plans), @supabase/ssr v0.5+, Next.js 14+ (App Router)
- Last updated: March 2026

## TL;DR

To integrate Supabase Auth with Next.js App Router, install @supabase/ssr, create a utility that initializes the Supabase client with cookie-based session storage, set up middleware for automatic token refresh, and build login and signup pages using signInWithPassword and signUp. The @supabase/ssr package handles cookie management so sessions persist across server and client components.

## Integrating Supabase Auth with Next.js App Router

Next.js App Router uses server components by default, which means traditional localStorage-based auth does not work. Supabase provides the @supabase/ssr package specifically for server-side rendering frameworks. This tutorial walks through the full setup: creating server and browser Supabase clients, building middleware for session refresh, and implementing login and signup flows that work seamlessly across server and client components.

## Before you start

- A Supabase project with email/password auth enabled
- A Next.js 14+ project using the App Router
- Node.js 18+ installed
- NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY from your Supabase Dashboard

## Step-by-step guide

### 1. Install @supabase/ssr and @supabase/supabase-js

The @supabase/ssr package provides cookie-based session management that works with server-side rendering. It replaces the default localStorage storage with cookies, which are automatically sent with every request to your Next.js server. Install both packages as dependencies in your Next.js project.

```
npm install @supabase/supabase-js @supabase/ssr
```

**Expected result:** Both packages are installed and listed in your package.json dependencies.

### 2. Set environment variables for your Supabase project

Create a .env.local file in the root of your Next.js project. Add your Supabase project URL and anon key. The NEXT_PUBLIC_ prefix makes these available in both server and client components. Find these values in the Supabase Dashboard under Settings > API. Never add the service role key with the NEXT_PUBLIC_ prefix — it bypasses RLS and should only be used in server-side code.

```
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

**Expected result:** Environment variables are accessible via process.env.NEXT_PUBLIC_SUPABASE_URL and process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY.

### 3. Create the Supabase client utilities for server and browser

Create two utility functions: one for server components and server actions that reads cookies from the Next.js headers, and one for client components that uses browser cookies. The server client uses the cookies() function from next/headers to read and write session cookies. The browser client uses the default cookie handling provided by @supabase/ssr. Place both in a lib/supabase/ directory.

```
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // The `setAll` method is called from a Server Component
            // where cookies cannot be set. This can be ignored.
          }
        },
      },
    }
  )
}

// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}
```

**Expected result:** You have two client utilities: one for server contexts and one for browser contexts, both using cookie-based session storage.

### 4. Create middleware for automatic session refresh

Next.js middleware runs on every request before it reaches your page. Use it to refresh expired Supabase sessions automatically. The middleware reads the session cookie, calls getUser() to validate and refresh the token, and writes updated cookies to the response. Without this middleware, users get logged out when their JWT expires even though a valid refresh token exists in the cookie.

```
// middleware.ts (in project root)
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const { data: { user } } = await supabase.auth.getUser()

  if (!user && !request.nextUrl.pathname.startsWith('/login') &&
      !request.nextUrl.pathname.startsWith('/signup')) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }

  return supabaseResponse
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
}
```

**Expected result:** Sessions are automatically refreshed on every page navigation. Unauthenticated users are redirected to the login page.

### 5. Build the login page with signInWithPassword

Create a login page that uses a server action to authenticate users. Server actions keep your auth logic on the server and prevent exposing any sensitive operations to the client. The action calls signInWithPassword, checks for errors, and redirects to the dashboard on success. Use the server Supabase client so the session cookie is set correctly.

```
// app/login/page.tsx
export default function LoginPage() {
  return (
    <form action="/auth/login" method="post">
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      <label htmlFor="password">Password</label>
      <input id="password" name="password" type="password" required />
      <button type="submit">Log in</button>
    </form>
  )
}

// app/auth/login/route.ts
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
  const formData = await request.formData()
  const email = formData.get('email') as string
  const password = formData.get('password') as string

  const supabase = await createClient()
  const { error } = await supabase.auth.signInWithPassword({ email, password })

  if (error) {
    return NextResponse.redirect(new URL('/login?error=Invalid+credentials', request.url))
  }

  return NextResponse.redirect(new URL('/dashboard', request.url))
}
```

**Expected result:** Users can log in with email and password. Successful login redirects to the dashboard with a valid session cookie.

### 6. Build the signup page and handle email confirmation

Create a signup page that calls supabase.auth.signUp. By default, Supabase requires email confirmation before the session is active. After signup, redirect users to a confirmation pending page. When the user clicks the confirmation link in their email, Supabase redirects them to your callback URL where you exchange the code for a session.

```
// app/auth/signup/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
  const formData = await request.formData()
  const email = formData.get('email') as string
  const password = formData.get('password') as string

  const supabase = await createClient()
  const { error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      emailRedirectTo: `${new URL(request.url).origin}/auth/callback`,
    },
  })

  if (error) {
    return NextResponse.redirect(new URL('/signup?error=Signup+failed', request.url))
  }

  return NextResponse.redirect(new URL('/signup/confirm', request.url))
}

// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const code = searchParams.get('code')

  if (code) {
    const supabase = await createClient()
    await supabase.auth.exchangeCodeForSession(code)
  }

  return NextResponse.redirect(new URL('/dashboard', request.url))
}
```

**Expected result:** New users receive a confirmation email. Clicking the link exchanges the code for a session and redirects to the dashboard.

## Complete code example

File: `lib/supabase/server.ts`

```typescript
// lib/supabase/server.ts
// Server-side Supabase client for Next.js App Router
// Uses cookies for session persistence across server components

import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Server Components cannot set cookies.
            // This is handled by the middleware instead.
          }
        },
      },
    }
  )
}

// Example usage in a Server Component:
//
// import { createClient } from '@/lib/supabase/server'
//
// export default async function DashboardPage() {
//   const supabase = await createClient()
//   const { data: { user } } = await supabase.auth.getUser()
//
//   if (!user) redirect('/login')
//
//   const { data: posts } = await supabase
//     .from('posts')
//     .select('*')
//     .eq('author_id', user.id)
//
//   return <div>{posts?.map(p => <p key={p.id}>{p.title}</p>)}</div>
// }
```

## Common mistakes

- **Using getSession() instead of getUser() on the server to check authentication** — undefined Fix: Always use getUser() on the server. getSession() reads from cookies without verification and can be tampered with. getUser() validates the JWT with Supabase's auth server.
- **Using the deprecated @supabase/auth-helpers-nextjs package instead of @supabase/ssr** — undefined Fix: Uninstall @supabase/auth-helpers-nextjs and install @supabase/ssr. The helpers package is no longer maintained and does not support the latest Next.js App Router patterns.
- **Forgetting to add the auth callback URL to the Supabase Dashboard redirect allowlist** — undefined Fix: Go to Authentication > URL Configuration > Redirect URLs in the Supabase Dashboard and add your callback URL (e.g., http://localhost:3000/auth/callback for development).
- **Creating the Supabase client at module scope instead of inside a function, causing stale sessions** — undefined Fix: Always create the Supabase client inside the function or component that uses it. Module-scope clients cache cookies from the first request and serve stale sessions to subsequent requests.

## Best practices

- Create the Supabase client inside each function call, never at module scope, to avoid stale cookie data
- Use getUser() for all server-side auth checks and reserve getSession() for non-critical client-side reads
- Set up middleware to refresh sessions on every request so users are not unexpectedly logged out
- Store only the anon key with the NEXT_PUBLIC_ prefix and keep the service role key in server-only environment variables
- Add your callback URL to the Supabase Dashboard redirect allowlist for both development and production
- Use the PKCE auth flow (default in @supabase/ssr) for enhanced security with SSR applications
- Handle the email confirmation flow with a dedicated callback route that exchanges the code for a session

## Frequently asked questions

### Why do I need @supabase/ssr instead of the regular @supabase/supabase-js?

The regular client uses localStorage for session storage, which does not exist on the server. @supabase/ssr uses cookies instead, which are automatically sent with every request to your Next.js server, enabling seamless auth across server and client components.

### Can I use Supabase Auth with the Next.js Pages Router?

Yes, @supabase/ssr works with both App Router and Pages Router. For Pages Router, use getServerSideProps to create the server client and pass the session as props. The middleware setup is the same.

### How do I protect a server component from unauthenticated access?

In the server component, create the Supabase client with createClient(), call getUser(), and redirect to the login page if user is null. The middleware also handles this globally, but component-level checks add defense in depth.

### Why does my session disappear after a page refresh?

This usually means the middleware is not set up correctly or the cookie matcher pattern is too restrictive. Ensure the middleware runs on all routes except static assets, and verify that the setAll function properly writes cookies to the response.

### Can I use social OAuth providers like Google with this setup?

Yes. Call supabase.auth.signInWithOAuth({ provider: 'google' }) from a client component. The OAuth flow redirects to the provider and back to your callback URL, where the middleware handles the session.

### Can RapidDev help set up Supabase Auth in my Next.js application?

Yes, RapidDev specializes in Supabase and Next.js integrations. They can set up authentication, configure middleware, implement role-based access control, and ensure your auth flow is production-ready.

### How do I deploy this to Vercel with the correct environment variables?

Use the Supabase Vercel Integration from the Vercel marketplace. It automatically syncs NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY to your Vercel project. Alternatively, add them manually in the Vercel Dashboard under Settings > Environment Variables.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-set-up-supabase-auth-with-next-js
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-set-up-supabase-auth-with-next-js
