Setting up Next.js Auth with Supabase — the correct way in Next.js 13+ (2025)
M. KADI

M. KADI

Full Stack Developer · Apr 8, 2025

Setting up Next.js Auth with Supabase — the correct way in Next.js 13+ (2025)

Supabase has become one of the most powerful open-source Firebase alternatives. Paired with Next.js 13+ App Router, it provides a clean and modern way to manage authentication, especially when you're building server-first applications. In this guide, you'll learn how to set up authentication using Supabase the right way — with SSR, protected routes, middleware, and a complete login/signup flow in a production-grade Next.js project.

1. Install Supabase Packages

First, install the required Supabase libraries. The core library @supabase/supabase-js is your connection to the Supabase API, while @supabase/ssr helps with server-side handling in the App Router.

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

2. Set Up Environment Variables

Create a .env.local file at the root of your project and add the following environment variables. These values come from your Supabase project's settings dashboard. Be sure to never expose your service role key to the browser.

NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
          NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

3. Create Supabase Utility Clients

In a Next.js 13 App Router project, it's essential to create two utility clients: one for the server and one for the client. These allow you to seamlessly call Supabase from wherever needed without rewriting the logic. This is especially important for authenticated routes.

utils/supabase/server.ts

import { createServerClient } from '@supabase/ssr'
          import { cookies } from 'next/headers'
          
          export const createClient = () => {
            return createServerClient(
              process.env.NEXT_PUBLIC_SUPABASE_URL!,
              process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
              { cookies }
            )
          }

utils/supabase/client.ts

import { createBrowserClient } from '@supabase/ssr'
          
          export const createClient = () => {
            return createBrowserClient(
              process.env.NEXT_PUBLIC_SUPABASE_URL!,
              process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
            )
          }

4. Middleware to Refresh Sessions

Since the server cannot refresh tokens (because cookies can't be mutated in Server Components), you’ll need middleware to ensure that session tokens remain fresh. Supabase provides a helper to do this automatically.

// middleware.ts
          import { type NextRequest } from 'next/server'
          import { updateSession } from '@/utils/supabase/middleware'
          
          export async function middleware(request: NextRequest) {
            return await updateSession(request)
          }
          
          export const config = {
            matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
          }

5. Build Login & Signup Forms

Create login and signup forms using Supabase actions inside route handlers. Be mindful to use server-side actions and not directly expose Supabase logic to the client unless necessary.

app/login/actions.ts

'use server'
          import { createClient } from '@/utils/supabase/server'
          
          export async function login(formData: FormData) {
            const supabase = createClient()
            const email = formData.get('email') as string
            const password = formData.get('password') as string
          
            const { error } = await supabase.auth.signInWithPassword({
              email,
              password
            })
          
            if (error) throw new Error(error.message)
          }

6. Protect Private Routes

One of the key benefits of SSR in Next.js 13 is that you can conditionally render pages server-side based on authentication. Here’s an example that ensures a route is only accessible to logged-in users.

app/dashboard/page.tsx

import { redirect } from 'next/navigation'
          import { createClient } from '@/utils/supabase/server'
          
          export default async function DashboardPage() {
            const supabase = createClient()
            const { data: { user } } = await supabase.auth.getUser()
          
            if (!user) {
              redirect('/login')
            }
          
            return <p className="text-xl">Welcome, {user.email}</p>
          }

7. Confirm Signups via Route Handler

When email confirmation is turned on (default in Supabase), new users must confirm their email address. Supabase sends a confirmation link, and your app should have a route ready to receive that token and complete the login.

app/auth/confirm/route.ts

import { createClient } from '@/utils/supabase/server'
          import { redirect } from 'next/navigation'
          
          export async function GET(request: Request) {
            const { searchParams } = new URL(request.url)
            const token = searchParams.get('token_hash')
            const type = searchParams.get('type')
          
            const supabase = createClient()
            const { error } = await supabase.auth.verifyOtp({
              token_hash: token!,
              type: type as any,
            })
          
            if (!error) {
              redirect('/dashboard')
            }
          
            redirect('/error')
          }

Conclusion

Supabase + Next.js 13 App Router is a powerful combination that allows you to build secure, server-rendered apps with rich user authentication, fully managed sessions, and cookie-based access control — all without the need for external libraries. By following the patterns in this guide, your setup will scale gracefully, stay secure, and integrate cleanly into your SSR Next.js workflow.

Now that you've learned how to do it the right way — go build something great.