Next.js Integration¶
Supabase SSR Package¶
Để sử dụng Supabase Auth với Next.js App Router, cần package @supabase/ssr.
Client Setup¶
Browser Client¶
// 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!
);
}
Server Client¶
// 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 {
// Called from Server Component
}
},
},
}
);
}
Middleware for Session Refresh¶
// middleware.ts
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 }) =>
request.cookies.set(name, value)
);
supabaseResponse = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
);
},
},
}
);
// Refresh session if expired
const { data: { user } } = await supabase.auth.getUser();
// Protect routes
const protectedPaths = ['/dashboard', '/settings', '/projects'];
const isProtectedPath = protectedPaths.some(path =>
request.nextUrl.pathname.startsWith(path)
);
if (isProtectedPath && !user) {
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('redirect', request.nextUrl.pathname);
return NextResponse.redirect(url);
}
return supabaseResponse;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};
Login Page¶
// app/(auth)/login/page.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { createClient } from '@/lib/supabase/client';
export default function LoginPage() {
const router = useRouter();
const supabase = createClient();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleEmailLogin(formData: FormData) {
setLoading(true);
setError(null);
const { error } = await supabase.auth.signInWithPassword({
email: formData.get('email') as string,
password: formData.get('password') as string,
});
if (error) {
setError(error.message);
setLoading(false);
return;
}
router.push('/dashboard');
router.refresh();
}
async function handleGoogleLogin() {
await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
}
return (
<div className="max-w-md mx-auto mt-20">
<h1 className="text-2xl font-bold mb-6">Login</h1>
{error && (
<div className="bg-red-100 text-red-700 p-3 rounded mb-4">
{error}
</div>
)}
<form action={handleEmailLogin} className="space-y-4">
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
required
className="w-full border p-2 rounded"
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
name="password"
type="password"
required
className="w-full border p-2 rounded"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-500 text-white p-2 rounded"
>
{loading ? 'Loading...' : 'Login'}
</button>
</form>
<div className="mt-4">
<button
onClick={handleGoogleLogin}
className="w-full border p-2 rounded"
>
Continue with Google
</button>
</div>
</div>
);
}
OAuth Callback¶
// app/auth/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
export async function GET(request: NextRequest) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get('code');
const next = searchParams.get('next') ?? '/dashboard';
if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(`${origin}${next}`);
}
}
// Return to error page
return NextResponse.redirect(`${origin}/auth/error`);
}
Protected Server Component¶
// app/dashboard/page.tsx
import { redirect } from 'next/navigation';
import { createClient } from '@/lib/supabase/server';
export default async function DashboardPage() {
const supabase = await createClient();
// Get authenticated user
const { data: { user }, error } = await supabase.auth.getUser();
if (!user) {
redirect('/login');
}
// Fetch user's data (RLS applied automatically)
const { data: tasks } = await supabase
.from('tasks')
.select('*')
.order('created_at', { ascending: false });
return (
<div>
<h1>Welcome, {user.email}</h1>
<h2>Your Tasks</h2>
<ul>
{tasks?.map(task => (
<li key={task.id}>{task.title}</li>
))}
</ul>
</div>
);
}
Logout¶
// components/LogoutButton.tsx
'use client';
import { useRouter } from 'next/navigation';
import { createClient } from '@/lib/supabase/client';
export function LogoutButton() {
const router = useRouter();
const supabase = createClient();
async function handleLogout() {
await supabase.auth.signOut();
router.push('/login');
router.refresh();
}
return (
<button onClick={handleLogout}>
Logout
</button>
);
}
Auth Context (Optional)¶
// contexts/AuthContext.tsx
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { User } from '@supabase/supabase-js';
import { createClient } from '@/lib/supabase/client';
interface AuthContextType {
user: User | null;
loading: boolean;
}
const AuthContext = createContext<AuthContextType>({
user: null,
loading: true,
});
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const supabase = createClient();
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null);
setLoading(false);
});
// Listen for changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user ?? null);
setLoading(false);
}
);
return () => subscription.unsubscribe();
}, []);
return (
<AuthContext.Provider value={{ user, loading }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
File Structure¶
app/
├── (auth)/
│ ├── login/
│ │ └── page.tsx
│ ├── register/
│ │ └── page.tsx
│ └── layout.tsx # Auth layout (no nav)
│
├── (dashboard)/
│ ├── dashboard/
│ │ └── page.tsx
│ ├── settings/
│ │ └── page.tsx
│ └── layout.tsx # Dashboard layout (with nav)
│
├── auth/
│ ├── callback/
│ │ └── route.ts # OAuth callback
│ └── confirm/
│ └── route.ts # Email confirmation
│
└── layout.tsx # Root layout
lib/
└── supabase/
├── client.ts # Browser client
└── server.ts # Server client
middleware.ts # Session refresh + protection
Tổng kết¶
Key Points¶
- Use @supabase/ssr for Next.js App Router
- Separate clients for browser/server
- Middleware refreshes sessions automatically
- Protect routes in middleware or components
- OAuth callback exchanges code for session
Auth Flow¶
Login → Supabase Auth → JWT → Cookie
↓
Middleware → Refresh token → Update cookie
↓
Server Component → getUser() → RLS
Q&A¶
- Có experience với Next.js middleware chưa?
- Cần OAuth providers nào?
- Questions về session handling?