Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

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

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.

What you'll learn

  • How to configure @supabase/ssr for cookie-based auth in Next.js App Router
  • How to create server and client Supabase clients with proper session handling
  • How to build middleware that refreshes tokens on every request
  • How to implement login and signup pages with email and password
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read20-25 minSupabase (all plans), @supabase/ssr v0.5+, Next.js 14+ (App Router)March 2026RapidDev Engineering Team
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.

Prerequisites

  • 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.

typescript
1npm 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.

typescript
1# .env.local
2NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
3NEXT_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.

typescript
1// lib/supabase/server.ts
2import { createServerClient } from '@supabase/ssr'
3import { cookies } from 'next/headers'
4
5export async function createClient() {
6 const cookieStore = await cookies()
7
8 return createServerClient(
9 process.env.NEXT_PUBLIC_SUPABASE_URL!,
10 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
11 {
12 cookies: {
13 getAll() {
14 return cookieStore.getAll()
15 },
16 setAll(cookiesToSet) {
17 try {
18 cookiesToSet.forEach(({ name, value, options }) =>
19 cookieStore.set(name, value, options)
20 )
21 } catch {
22 // The `setAll` method is called from a Server Component
23 // where cookies cannot be set. This can be ignored.
24 }
25 },
26 },
27 }
28 )
29}
30
31// lib/supabase/client.ts
32import { createBrowserClient } from '@supabase/ssr'
33
34export function createClient() {
35 return createBrowserClient(
36 process.env.NEXT_PUBLIC_SUPABASE_URL!,
37 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
38 )
39}

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.

typescript
1// middleware.ts (in project root)
2import { createServerClient } from '@supabase/ssr'
3import { NextResponse, type NextRequest } from 'next/server'
4
5export async function middleware(request: NextRequest) {
6 let supabaseResponse = NextResponse.next({ request })
7
8 const supabase = createServerClient(
9 process.env.NEXT_PUBLIC_SUPABASE_URL!,
10 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
11 {
12 cookies: {
13 getAll() {
14 return request.cookies.getAll()
15 },
16 setAll(cookiesToSet) {
17 cookiesToSet.forEach(({ name, value, options }) =>
18 request.cookies.set(name, value)
19 )
20 supabaseResponse = NextResponse.next({ request })
21 cookiesToSet.forEach(({ name, value, options }) =>
22 supabaseResponse.cookies.set(name, value, options)
23 )
24 },
25 },
26 }
27 )
28
29 const { data: { user } } = await supabase.auth.getUser()
30
31 if (!user && !request.nextUrl.pathname.startsWith('/login') &&
32 !request.nextUrl.pathname.startsWith('/signup')) {
33 const url = request.nextUrl.clone()
34 url.pathname = '/login'
35 return NextResponse.redirect(url)
36 }
37
38 return supabaseResponse
39}
40
41export const config = {
42 matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
43}

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.

typescript
1// app/login/page.tsx
2export default function LoginPage() {
3 return (
4 <form action="/auth/login" method="post">
5 <label htmlFor="email">Email</label>
6 <input id="email" name="email" type="email" required />
7 <label htmlFor="password">Password</label>
8 <input id="password" name="password" type="password" required />
9 <button type="submit">Log in</button>
10 </form>
11 )
12}
13
14// app/auth/login/route.ts
15import { createClient } from '@/lib/supabase/server'
16import { redirect } from 'next/navigation'
17import { NextResponse } from 'next/server'
18
19export async function POST(request: Request) {
20 const formData = await request.formData()
21 const email = formData.get('email') as string
22 const password = formData.get('password') as string
23
24 const supabase = await createClient()
25 const { error } = await supabase.auth.signInWithPassword({ email, password })
26
27 if (error) {
28 return NextResponse.redirect(new URL('/login?error=Invalid+credentials', request.url))
29 }
30
31 return NextResponse.redirect(new URL('/dashboard', request.url))
32}

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.

typescript
1// app/auth/signup/route.ts
2import { createClient } from '@/lib/supabase/server'
3import { NextResponse } from 'next/server'
4
5export async function POST(request: Request) {
6 const formData = await request.formData()
7 const email = formData.get('email') as string
8 const password = formData.get('password') as string
9
10 const supabase = await createClient()
11 const { error } = await supabase.auth.signUp({
12 email,
13 password,
14 options: {
15 emailRedirectTo: `${new URL(request.url).origin}/auth/callback`,
16 },
17 })
18
19 if (error) {
20 return NextResponse.redirect(new URL('/signup?error=Signup+failed', request.url))
21 }
22
23 return NextResponse.redirect(new URL('/signup/confirm', request.url))
24}
25
26// app/auth/callback/route.ts
27import { createClient } from '@/lib/supabase/server'
28import { NextResponse } from 'next/server'
29
30export async function GET(request: Request) {
31 const { searchParams } = new URL(request.url)
32 const code = searchParams.get('code')
33
34 if (code) {
35 const supabase = await createClient()
36 await supabase.auth.exchangeCodeForSession(code)
37 }
38
39 return NextResponse.redirect(new URL('/dashboard', request.url))
40}

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

Complete working example

lib/supabase/server.ts
1// lib/supabase/server.ts
2// Server-side Supabase client for Next.js App Router
3// Uses cookies for session persistence across server components
4
5import { createServerClient } from '@supabase/ssr'
6import { cookies } from 'next/headers'
7
8export async function createClient() {
9 const cookieStore = await cookies()
10
11 return createServerClient(
12 process.env.NEXT_PUBLIC_SUPABASE_URL!,
13 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
14 {
15 cookies: {
16 getAll() {
17 return cookieStore.getAll()
18 },
19 setAll(cookiesToSet) {
20 try {
21 cookiesToSet.forEach(({ name, value, options }) =>
22 cookieStore.set(name, value, options)
23 )
24 } catch {
25 // Server Components cannot set cookies.
26 // This is handled by the middleware instead.
27 }
28 },
29 },
30 }
31 )
32}
33
34// Example usage in a Server Component:
35//
36// import { createClient } from '@/lib/supabase/server'
37//
38// export default async function DashboardPage() {
39// const supabase = await createClient()
40// const { data: { user } } = await supabase.auth.getUser()
41//
42// if (!user) redirect('/login')
43//
44// const { data: posts } = await supabase
45// .from('posts')
46// .select('*')
47// .eq('author_id', user.id)
48//
49// return <div>{posts?.map(p => <p key={p.id}>{p.title}</p>)}</div>
50// }

Common mistakes when setting up Supabase Auth with Next.js App Router

Why it's a problem: Using getSession() instead of getUser() on the server to check authentication

How to avoid: 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.

Why it's a problem: Using the deprecated @supabase/auth-helpers-nextjs package instead of @supabase/ssr

How to avoid: 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.

Why it's a problem: Forgetting to add the auth callback URL to the Supabase Dashboard redirect allowlist

How to avoid: 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).

Why it's a problem: Creating the Supabase client at module scope instead of inside a function, causing stale sessions

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I am setting up Supabase Auth in a Next.js 14 App Router project. Walk me through installing @supabase/ssr, creating server and browser client utilities, setting up middleware for session refresh, and building login and signup pages with email/password authentication.

Supabase Prompt

Configure @supabase/ssr in my Next.js App Router project. Create the server client utility using cookies, add middleware for automatic session refresh with redirect to /login for unauthenticated users, and build a login route handler using signInWithPassword.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.