Next.js Project Supabase Auth Integration Guide
Based on the official Supabase Next.js authentication guide and the actual code from the Nexty.dev template, this documentation will detail the code integration steps and usage methods for Supabase Auth in Next.js projects.
Detailed Integration Steps
Step 1: Install Dependencies
npm install @supabase/supabase-js @supabase/ssr
Package descriptions:
@supabase/supabase-js
- Supabase JavaScript client library@supabase/ssr
- Helper library optimized for server-side rendering
Step 2: Environment Variable Configuration
Create a .env.local
file and configure the necessary environment variables:
# Supabase basic configuration
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
For detailed environment variable descriptions, please see: Environment Variables
Step 3: Create Supabase Clients
3.1 Browser Client
import { Database } from '@/lib/supabase/types'
import { createBrowserClient } from '@supabase/ssr'
export const createClient = () => {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
This is a client dedicated to the browser environment, used for:
- Handling user interactions (login, registration, data updates)
- Listening to real-time subscription data changes
- Providing authentication state management
3.2 Server-side Client
import { Database } from '@/lib/supabase/types'
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
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 {
// setAll calls in Server Components will be ignored
}
},
},
}
)
}
This is a client dedicated to server-side components, used for:
- Database operations in Server Actions
- API handling in Route Handlers
- Managing user sessions through Cookies
Step 4: Set Up Middleware Authentication
We need to first complete the access control logic required by the middleware based on Supabase functionality.
/*
* This shows the core logic, not the complete file code
*/
// Routes requiring admin privileges
const ROLE_PROTECTED_ROUTES: Record<string, string[]> = {
'dashboard/users': ['admin'],
};
// Routes requiring login
const PROTECTED_ROUTES = ['dashboard'];
export async function updateSession(request: NextRequest) {
// Get user information
const { data: { user } } = await supabase.auth.getUser()
// Check login status
if (!user && isProtectedRoute) {
return NextResponse.redirect(new URL("/login", request.url))
}
// Check role permissions
if (user && needsRoleCheck) {
const { data } = await supabase.from("users").select("role").eq("id", user.id).single();
if (!allowedRoles.includes(data?.role || "user")) {
return NextResponse.redirect(new URL("/403", request.url))
}
}
}
This file provides authentication state management logic:
- Check and update user authentication status through
updateSession()
- Handle token refresh, session validation, etc.
It also provides access control logic:
- Admin-only pages are forbidden to other users
- Logged-in user pages are forbidden to unauthenticated users
- Redirect to login page with callback address, allowing users to directly enter target page after successful login
Import lib/supabase/middleware.ts
into middleware.ts
to make the above logic effective.
import { updateSession } from '@/lib/supabase/middleware';
import createIntlMiddleware from 'next-intl/middleware';
import { NextRequest, NextResponse } from 'next/server';
import { routing } from './i18n/routing';
const intlMiddleware = createIntlMiddleware(routing);
export async function middleware(request: NextRequest): Promise<NextResponse> {
// 1. First handle Supabase authentication
const supabaseResponse = await updateSession(request);
if (supabaseResponse.headers.get('location')) {
return supabaseResponse;
}
// 2. Then handle internationalization routing
const intlResponse = intlMiddleware(request);
// 3. Merge cookies
supabaseResponse.cookies.getAll().forEach((cookie) => {
const { name, value, ...options } = cookie;
intlResponse.cookies.set(name, value, options);
});
return intlResponse;
}
This completes the full middleware requirements:
- Automatically refresh expired authentication tokens
- Permission checking, protecting routes that require login
- Role verification, checking admin privileges
- Internationalization integration
Step 5: Frontend Authentication State Management
In the [locale]/layout.tsx
file, we introduce <AuthProvider>
to implement global frontend authentication state management.
5.1 Core Features of AuthProvider
type AuthContextType = {
user: ExtendedUser | null; // User object with role information
loading: boolean; // Loading state
signInWithGoogle: () => Promise<{ error: AuthError | null }>;
signInWithGithub: () => Promise<{ error: AuthError | null }>;
signInWithEmail: (email: string, captchaToken?: string) => Promise<{ error: AuthError | null }>;
signOut: () => Promise<void>;
refreshUser: () => Promise<void>; // Manually refresh user information
};
5.2 Extended User Information
The template extends Supabase's user type, adding role information:
type ExtendedUser = User & {
role: "admin" | "user";
};
const fetchUserRole = async (userId: string) => {
const { data, error } = await supabase
.from("users")
.select("role")
.eq("id", userId)
.limit(1)
.maybeSingle();
return data?.role || "user";
};
If your frontend needs important user data from the User
table or other data tables, you can extend it in the fetchUserRole
method.
5.3 Real-time Role Updates
The template implements real-time listening for role changes:
const userSubscription = supabase
.channel("public:users")
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "public",
table: "users",
filter: `id=eq.${user?.id}`,
},
async (payload) => {
if (user) {
setUser({
...user,
role: payload.new.role,
});
}
}
)
.subscribe();
5.4 Multiple Login Methods Support
Social Login:
const signInWithGoogle = async () => {
return await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${window.location.origin}/auth/callback?next=${next}`,
},
});
};
const signInWithGithub = async () => {
return await supabase.auth.signInWithOAuth({
provider: "github",
options: {
redirectTo: `${window.location.origin}/auth/callback?next=${next}`,
},
});
};
Email Magic Link:
const signInWithEmail = async (email: string, captchaToken?: string) => {
return await supabase.auth.signInWithOtp({
email: normalizeEmail(email),
options: {
emailRedirectTo: `${window.location.origin}/auth/callback?next=${next}`,
captchaToken,
},
});
};
Step 6: Authentication Callback Handling
The template implements a complete authentication callback handling mechanism.
6.1 OAuth Callback
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
let next = searchParams.get('next') ?? '/'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(`${origin}${next}`)
}
}
return NextResponse.redirect(new URL(`/redirect-error?code=server_error`, origin))
}
6.2 Email Confirmation
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const token_hash = searchParams.get('token_hash')
const type = searchParams.get('type') as EmailOtpType | null
if (!token_hash || !type) {
return NextResponse.redirect(new URL(`/redirect-error?code=invalid_params`, origin))
}
const supabase = await createClient()
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
})
if (error) {
// Detailed error handling
switch (error.message) {
case 'Token has expired':
return NextResponse.redirect(new URL(`/redirect-error?code=token_expired`, origin))
// ... other error handling
}
}
return NextResponse.redirect(new URL(next, origin))
}
Application in Real Projects
Using in Client Components
import { useAuth } from '@/components/providers/AuthProvider';
export default function UserProfile() {
const { user, loading, signOut } = useAuth();
if (loading) return <div>Loading...</div>;
if (!user) {
return <LoginButton />;
}
return (
<div>
<h1>Welcome, {user.email}</h1>
<p>Role: {user.role}</p>
{user.role === 'admin' && <AdminPanel />}
<button onClick={signOut}>Sign Out</button>
</div>
);
}
Using in Server Components
import { createClient } from '@/lib/supabase/server';
import { redirect } from 'next/navigation';
export default async function PrivatePage() {
const supabase = await createClient();
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) {
redirect('/login');
}
return <p>Hello {user.email}</p>;
}
Using in Server Actions
'use server'
import { createClient as createAdminClient } from "@supabase/supabase-js";
import { isAdmin } from '@/lib/supabase/isAdmin';
export async function adminAction() {
if (!(await isAdmin())) {
return actionResponse.forbidden("Admin privileges required.");
}
const supabaseAdmin = createAdminClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
// Execute admin operations
}
Security Best Practices
1. Use getUser()
instead of getSession()
on Server-side
// ✅ Correct - Server-side always validates tokens
const { data: { user } } = await supabase.auth.getUser();
// ❌ Wrong - Should not be used on server-side, tokens may not be validated
const { data: { session } } = await supabase.auth.getSession();
2. Separate Client and Server-side Logic
// Client component - use browser client
const supabase = createClient(); // from @/lib/supabase/client
// Server component/Actions - use server-side client
const supabase = await createClient(); // from @/lib/supabase/server
3. Use Service Role Key After Authentication for Sensitive Operations
For sensitive operations, first verify admin identity, then use Service Role Key to operate the database
import { createClient as createAdminClient } from "@supabase/supabase-js";
import { isAdmin } from '@/lib/supabase/isAdmin';
// Verify admin identity
if (!(await isAdmin())) {
return actionResponse.forbidden("Admin privileges required.");
}
// Admin uses Service Role Key to operate database
const supabaseAdmin = createAdminClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // Server-side only
);