Next.js Setup
Supabase SSR Package
Installation
npm install @supabase/supabase-js @supabase/ssr
Why @supabase/ssr?
┌─────────────────────────────────────────────────────────────┐
│ SUPABASE SSR │
├─────────────────────────────────────────────────────────────┤
│ │
│ Standard @supabase/supabase-js: │
│ - Stores session in localStorage │
│ - Works only in browser │
│ │
│ @supabase/ssr: │
│ - Stores session in cookies │
│ - Works in Server Components, Route Handlers, Middleware │
│ - Proper SSR authentication │
│ │
└─────────────────────────────────────────────────────────────┘
Project Structure
Client Setup Files
lib/
└── supabase/
├── client.ts # Browser client
├── server.ts # Server client
└── middleware.ts # Middleware client
Browser Client
lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';
import { Database } from '@/types/supabase';
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
Usage in Client Components
'use client';
import { createClient } from '@/lib/supabase/client';
import { useEffect, useState } from 'react';
export function TaskList() {
const [tasks, setTasks] = useState([]);
const supabase = createClient();
useEffect(() => {
async function fetchTasks() {
const { data } = await supabase
.from('tasks')
.select('*');
setTasks(data || []);
}
fetchTasks();
}, []);
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
);
}
Server Client
lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
import { Database } from '@/types/supabase';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient<Database>(
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 (error) {
// Called from Server Component - ignore
}
},
},
}
);
}
Usage in Server Components
// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const supabase = await createClient();
// Check auth
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
redirect('/login');
}
// Fetch data
const { data: tasks } = await supabase
.from('tasks')
.select('*')
.eq('user_id', user.id);
return (
<div>
<h1>Welcome, {user.email}</h1>
<ul>
{tasks?.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
</div>
);
}
Route Handlers
API Route Example
// app/api/tasks/route.ts
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
export async function GET() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { data, error } = await supabase
.from('tasks')
.select('*')
.eq('user_id', user.id);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data);
}
export async function POST(request: Request) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { data, error } = await supabase
.from('tasks')
.insert({
...body,
user_id: user.id,
})
.select()
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data, { status: 201 });
}
Middleware
lib/supabase/middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function updateSession(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, options }) => {
request.cookies.set(name, value);
});
supabaseResponse = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) => {
supabaseResponse.cookies.set(name, value, options);
});
},
},
}
);
// Refresh session
await supabase.auth.getUser();
return supabaseResponse;
}
middleware.ts
// middleware.ts (root)
import { type NextRequest } from 'next/server';
import { updateSession } from '@/lib/supabase/middleware';
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};
Protected Routes
Middleware Protection
// middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
const response = await updateSession(request);
// Create client for auth check
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll() {},
},
}
);
const { data: { user } } = await supabase.auth.getUser();
// Protect /dashboard routes
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
const redirectUrl = new URL('/login', request.url);
redirectUrl.searchParams.set('redirect', request.nextUrl.pathname);
return NextResponse.redirect(redirectUrl);
}
// Redirect logged in users from /login
if (user && request.nextUrl.pathname === '/login') {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return response;
}
Auth Callback Handler
app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
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}`);
}
}
// Auth error
return NextResponse.redirect(`${origin}/auth/error`);
}
Environment Variables
.env.local
# Public (exposed to browser)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
# Private (server-side only)
SUPABASE_SERVICE_ROLE_KEY=eyJhbGc... # Never expose!
Tổng kết
Client Types
| Client |
Use In |
Auth |
Browser (client.ts) |
Client Components |
User session |
Server (server.ts) |
Server Components, Route Handlers |
User session |
| Service Role |
Admin operations |
Full access |
Setup Checklist
- [ ] Install
@supabase/ssr
- [ ] Create
lib/supabase/client.ts
- [ ] Create
lib/supabase/server.ts
- [ ] Create
middleware.ts
- [ ] Create auth callback route
- [ ] Set environment variables
- [ ] Generate TypeScript types
Q&A
- Server Components hay Client Components chủ yếu?
- Cần middleware protection không?
- Auth flow hiện tại như thế nào?