
How to Add Supabase Auth in Next.js
Authentication is foundational infrastructure in any serious web application. In this guide, you’ll implement Supabase Authentication in a Next.js App Router project with a clean, scalable architecture suitable for production.
We will cover:
Project setup
Supabase client configuration (browser + server)
Auth provider pattern
Sign up / Sign in / Sign out
Protected routes (server + middleware)
Session handling in App Router
1. Create a Supabase Project
Go to Supabase and create a new project.
Navigate to Settings → API
Copy:
Project URLanon public key
Enable authentication providers (Email/Password is enabled by default).
2. Install Dependencies
npm install @supabase/supabase-js @supabase/ssrWe use:
@supabase/supabase-js→ Core client@supabase/ssr→ Proper session handling in App Router (server components)
3. Environment Variables
Create .env.local:
NEXT_PUBLIC_SUPABASE_URL=your_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_keyNever expose the service_role key to the browser.
4. Create Supabase Clients (Production Pattern)
We create two clients:
Browser client
Server client (for RSC + route handlers)
/lib/supabase/client.js
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
}/lib/supabase/server.js
import { cookies } from "next/headers";
import { createServerClient } from "@supabase/ssr";
export function createClient() {
const cookieStore = cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
cookies: {
get: (name) => cookieStore.get(name)?.value,
set: (name, value, options) =>
cookieStore.set({ name, value, ...options }),
remove: (name, options) =>
cookieStore.set({ name, value: "", ...options }),
},
}
);
}This ensures sessions work correctly in:
Server Components
Route Handlers
Middleware
5. Create Auth Context Provider
We manage auth state globally.
/context/AuthProvider.jsx
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";
const AuthContext = createContext();
export function AuthProvider({ children }) {
const supabase = createClient();
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const getUser = async () => {
const { data } = await supabase.auth.getUser();
setUser(data.user);
setLoading(false);
};
getUser();
const { data: listener } = supabase.auth.onAuthStateChange(
(_, session) => {
setUser(session?.user ?? null);
}
);
return () => listener.subscription.unsubscribe();
}, []);
return (
<AuthContext.Provider value={{ user, loading, supabase }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);6. Wrap Root Layout
/app/layout.js
import { AuthProvider } from "@/context/AuthProvider";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}7. Create Sign Up Page (Tailwind Responsive)
/app/signup/page.jsx
"use client";
import { useState } from "react";
import { useAuth } from "@/context/AuthProvider";
import { useRouter } from "next/navigation";
export default function SignupPage() {
const { supabase } = useAuth();
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSignup = async (e) => {
e.preventDefault();
const { error } = await supabase.auth.signUp({
email,
password,
});
if (!error) router.push("/dashboard");
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100 px-4">
<form onSubmit={handleSignup} className="w-full max-w-md bg-white p-6 rounded-xl shadow-lg space-y-4">
<h1 className="text-2xl font-semibold text-center">Create Account</h1>
<input type="email" placeholder="Email" className="w-full border p-3 rounded-lg" onChange={(e) => setEmail(e.target.value)} required />
<input type="password" placeholder="Password" className="w-full border p-3 rounded-lg" onChange={(e) => setPassword(e.target.value)} required />
<button type="submit" className="w-full bg-black text-white py-3 rounded-lg hover:opacity-90 transition">Sign Up</button>
</form>
</div>
);
}Fully responsive:
Mobile: full width
Tablet/Desktop: centered card
8. Protect a Route (Server Component)
/app/dashboard/page.jsx
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
export default async function Dashboard() {
const supabase = createClient();
const { data } = await supabase.auth.getUser();
if (!data.user) redirect("/login");
return <div className="p-6">Welcome {data.user.email}</div>;
}This is the correct production way to protect App Router pages.
9. Add Logout Button
"use client";
import { useAuth } from "@/context/AuthProvider";
export default function LogoutButton() {
const { supabase } = useAuth();
const handleLogout = async () => {
await supabase.auth.signOut();
window.location.href = "/";
};
return (
<button onClick={handleLogout} className="px-4 py-2 bg-red-500 text-white rounded-lg">
Logout
</button>
);
}10. Middleware Protection (Optional Global Protection)
Create middleware.js in root:
import { createMiddlewareClient } from "@supabase/ssr";
import { NextResponse } from "next/server";
export async function middleware(req) {
const res = NextResponse.next();
const supabase = createMiddlewareClient({ req, res });
const { data } = await supabase.auth.getUser();
if (!data.user && req.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", req.url));
}
return res;
}Final Architecture (Recommended Structure)
/app
/login
/signup
/dashboard
/lib/supabase
client.js
server.js
/context
AuthProvider.jsx
middleware.jsProduction Best Practices
Never expose service_role key
Always protect sensitive pages on the server
Use middleware for global protection
Enable email confirmation in Supabase
Use Row Level Security (RLS) in database
Add rate limiting for auth routes