Supabase Auth Integration Guide for Next.js
This guide builds upon the official Supabase Next.js authentication guide and includes practical examples from the Nexty.dev boilerplate to demonstrate step-by-step integration and usage patterns for Supabase Auth in Next.js projects.
Integration Steps
Step 1: Install Dependencies
npm install @supabase/supabase-js @supabase/ssr
Dependency Explanation:
@supabase/supabase-js
- Supabase JavaScript client library@supabase/ssr
- Helper library optimized for server-side rendering
Step 2: Environment Variables Configuration
Create a .env.local
file in your project root and add the following 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
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your_google_client_id
For detailed environment variable information, please read: Environment Variables
Step 3: Create Supabase Clients
3.1 Client-side Client
import { Database } from '@/lib/supabase/types'
import { createBrowserClient } from '@supabase/ssr'
export const createClient = () => {
// Create browser client for client-side operations
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
The browser client is designed for client-side operations and provides:
- User interaction handling (login, registration, data updates)
- Real-time data subscriptions and updates
- 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
}
},
},
}
)
}
The server client is optimized for server-side operations and handles:
- Database operations within Server Actions
- API request processing in Route Handlers
Step 4: Set Up Middleware Authentication
First, we'll implement the authentication middleware logic using Supabase's authentication features.
/*
* Core middleware authentication logic
* Note: This is a simplified example. See the complete implementation in the source code.
*/
// Admin-only routes configuration
const ROLE_PROTECTED_ROUTES: Record<string, string[]> = {
'dashboard/users': ['admin'],
};
// Routes that require authentication
const PROTECTED_ROUTES = ['dashboard'];
export async function updateSession(request: NextRequest) {
// Retrieve current user from Supabase
const { data: { user } } = await supabase.auth.getUser()
// Redirect unauthenticated users from protected routes
if (!user && isProtectedRoute) {
return NextResponse.redirect(new URL("/login", request.url))
}
// Verify user role for role-protected routes
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))
}
}
}
The lib/supabase/middleware.ts
file is imported by Next.js's middleware.ts
and runs on every request. It serves as the authentication gateway, handling:
- Cookie validation and refresh
- Session management
- User authentication status verification
Key responsibilities:
- Session Management: Validate and refresh user sessions via
updateSession()
- Cookie Handling: Manage authentication cookies and token refresh
- Route Protection: Implement access control for protected routes
Since this middleware runs on every request, it enables comprehensive access control:
Authentication Layers:
- Login Protection: Redirects unauthenticated users from protected routes
- Role-based Access: Restricts admin-only pages to authorized users
The complete middleware implementation integrates authentication with internationalization:
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 implementation provides comprehensive middleware functionality:
- Automatic token refresh for expired sessions
- Route protection for authenticated users
- Role-based access control for admin features
- Seamless internationalization integration
Step 5: Frontend Authentication State Management
The <AuthProvider>
component, integrated into [locale]/layout.tsx
, provides global authentication state management across the application.
5.1 Core Functions of AuthProvider
type AuthContextType = {
user: ExtendedUser | null; // User object with role information
loading: boolean; // Loading state
signInWithGoogle: (next?: string) => Promise<{ error: AuthError | null }>;
signInWithGithub: (next?: string) => Promise<{ error: AuthError | null }>;
signInWithEmail: (
email: string,
captchaToken?: string,
next?: string
) => Promise<{ error: AuthError | null }>;
signOut: () => Promise<void>;
refreshUser: () => Promise<void>;
};
5.2 Extended User Information
The boilerplate enhances Supabase's default user object by including role information from the database:
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";
};
Good to know
You can extend the
fetchUserRole
method to include additional user data from theusers
table or related tables as needed by your application.
5.3 Real-time Role Synchronization
The authentication provider includes real-time subscriptions to automatically sync role changes:
const userSubscription = supabase
.channel("public:users")
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "public",
table: "users",
filter: `id=eq.${user?.id}`,
},
async (payload) => {
setUser((prevUser) => {
if (prevUser) {
return {
...prevUser,
role: payload.new.role,
};
}
return prevUser;
});
}
)
.subscribe();
5.4 Authentication Methods
The provider supports multiple authentication strategies:
OAuth Providers:
const signInWithGoogle = async (next?: string) => {
const redirectUrl = new URL(`${window.location.origin}/auth/callback`);
const referral = getReferral();
redirectUrl.searchParams.set("referral", referral || "direct");
if (next) {
redirectUrl.searchParams.set("next", next);
}
return await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: redirectUrl.toString(),
},
});
};
const signInWithGithub = async (next?: string) => {
const redirectUrl = new URL(`${window.location.origin}/auth/callback`);
const referral = getReferral();
redirectUrl.searchParams.set("referral", referral || "direct");
if (next) {
redirectUrl.searchParams.set("next", next);
}
return await supabase.auth.signInWithOAuth({
provider: "github",
options: {
redirectTo: redirectUrl.toString(),
},
});
};
Email Magic Link:
const signInWithEmail = async (
email: string,
captchaToken?: string,
next?: string
) => {
const redirectUrl = new URL(`${window.location.origin}/auth/callback`);
const referral = getReferral();
redirectUrl.searchParams.set("referral", referral || "direct");
if (next) {
redirectUrl.searchParams.set("next", next);
}
return await supabase.auth.signInWithOtp({
email: normalizeEmail(email),
options: {
emailRedirectTo: redirectUrl.toString(),
captchaToken,
data: { referral },
},
});
};
Step 6: Authentication Callback Handling
The authentication system includes comprehensive callback handlers to process different authentication flows.
6.1 OAuth and Magic Link Callback Handler
This route handler processes authentication callbacks from OAuth providers (Google, GitHub) and magic link email authentication.
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 Verification Handler
This route processes email verification tokens for various scenarios:
- Email Registration: Confirming new user accounts
- Email Changes: Verifying email address updates
- Password Resets: Validating password reset requests
Good to know
While not used in the Nexty's auth flow, this handler is ready for traditional email/password authentication if you choose to implement it.
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))
}
Implementation Guide
Client Components
For client-side components, use the useAuth
hook to access user data and authentication methods:
import { useAuth } from '@/components/providers/AuthProvider';
export default function UserProfile() {
const { user, loading, signOut } = useAuth(); // Retrive user from 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>
);
}
Server Components
Server components must validate authentication server-side using supabase.auth.getUser()
to prevent cookie tampering:
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(); // Retrive user from supabase.auth.getUser()
if (error || !user) {
redirect('/login');
}
return <p>Hello {user.email}</p>;
}
Server Actions
In Server Actions, authentication for regular users can use supabase.auth.getUser()
, but for sensitive operations, you need to verify admin privileges first and then use the JS client package to operate the database, which is safer.
The lib/supabase/isAdmin
encapsulates the admin role verification method. In all places that require admin operation privileges, this method must be called first before performing database operations.
'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!
);
// Perform admin operations
}
Security Best Practices
1. Use getUser()
Instead of getSession()
on Server-side
// ✅ Correct - Server-side always validates token
const { data: { user } } = await supabase.auth.getUser();
// ❌ Wrong - Should not be used on server-side, token 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 for Sensitive Operations After Authentication
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
);