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.