6.4 KiB
6.4 KiB
Authentication Patterns for Next.js + Supabase
Common Authentication Patterns
Pattern 1: Protected Route Groups
Use Next.js route groups to protect multiple routes with a single layout:
app/
├── (public)/
│ ├── layout.tsx # Public layout
│ ├── page.tsx # Home page
│ └── about/
│ └── page.tsx # About page
└── (protected)/
├── layout.tsx # Protected layout with requireAuth()
├── dashboard/
│ └── page.tsx
└── settings/
└── page.tsx
Protected layout example:
// app/(protected)/layout.tsx
import { requireAuth } from '@/lib/auth/utils';
export default async function ProtectedLayout({
children,
}: {
children: React.ReactNode;
}) {
await requireAuth();
return <>{children}</>;
}
All routes inside (protected) automatically require authentication.
Pattern 2: Conditional UI Based on Auth State
Show different content based on whether user is authenticated:
import { getCurrentUser } from '@/lib/auth/utils';
export default async function HomePage() {
const user = await getCurrentUser();
return (
<div>
{user ? (
<div>
<h1>Welcome back, {user.email}!</h1>
<a href="/dashboard">Go to Dashboard</a>
</div>
) : (
<div>
<h1>Welcome to our app</h1>
<a href="/login">Sign in</a>
</div>
)}
</div>
);
}
Pattern 3: Role-Based Access Control
Protect routes based on user roles:
// lib/auth/utils.ts - Add role checking
export async function requireRole(allowedRoles: string[]) {
const user = await requireAuth();
const supabase = await createServerClient();
const { data: profile } = await supabase
.from('profiles')
.select('role')
.eq('id', user.id)
.single();
if (!profile || !allowedRoles.includes(profile.role)) {
redirect('/unauthorized');
}
return { user, profile };
}
Usage in admin page:
export default async function AdminPage() {
await requireRole(['admin', 'moderator']);
return <div>Admin Dashboard</div>;
}
Pattern 4: Server Action Authentication
Protect Server Actions that modify data:
'use server';
import { requireAuth } from '@/lib/auth/utils';
import { createServerClient } from '@/lib/supabase/server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const user = await requireAuth();
const supabase = await createServerClient();
const title = formData.get('title') as string;
const content = formData.get('content') as string;
const { error } = await supabase.from('posts').insert({
title,
content,
author_id: user.id,
});
if (error) throw error;
revalidatePath('/posts');
}
Pattern 5: API Route Authentication
Protect API routes for external access:
// app/api/protected/route.ts
import { createServerClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
export async function GET() {
const supabase = await createServerClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Return protected data
return NextResponse.json({ data: 'Protected data' });
}
Pattern 6: Parallel Data Fetching with Auth
Fetch user and their data in parallel:
export default async function ProfilePage() {
const user = await requireAuth();
const supabase = await createServerClient();
// Fetch user profile and posts in parallel
const [{ data: profile }, { data: posts }] = await Promise.all([
supabase.from('profiles').select('*').eq('id', user.id).single(),
supabase.from('posts').select('*').eq('author_id', user.id),
]);
return (
<div>
<h1>{profile?.name}</h1>
<h2>Posts</h2>
{posts?.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
Pattern 7: Redirect After Login
Preserve intended destination after login:
Middleware:
export async function middleware(request: NextRequest) {
const { supabaseResponse, user } = await updateSession(request);
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
const redirectUrl = new URL('/login', request.url);
// Save the intended destination
redirectUrl.searchParams.set('redirect', request.nextUrl.pathname);
return Response.redirect(redirectUrl);
}
return supabaseResponse;
}
Login page:
export default async function LoginPage({
searchParams,
}: {
searchParams: { redirect?: string };
}) {
// After successful login, redirect to intended page
const redirectTo = searchParams.redirect || '/dashboard';
// Use redirectTo in form action or success handler
}
Pattern 8: Email Verification Check
Require email verification before accessing certain features:
export async function requireVerifiedEmail() {
const user = await requireAuth();
if (!user.email_confirmed_at) {
redirect('/verify-email');
}
return user;
}
Best Practices
- Use Server Components for Auth Checks: Leverage Server Components for initial auth checks to avoid client-side flashing
- Middleware for Session Refresh: Always refresh sessions in middleware to keep auth state fresh
- Revalidate After Auth Changes: Use
revalidatePath()after login/logout to clear cached content - Secure Cookie Configuration: Ensure cookies use secure, httpOnly, and sameSite settings
- Handle Auth Errors Gracefully: Provide clear error messages and recovery paths
- Use TypeScript: Type your user objects and auth functions for better DX
- Test Protected Routes: Verify both authenticated and unauthenticated access
- Implement Proper Redirects: Always redirect after authentication changes to prevent stale UI
Anti-Patterns to Avoid
- Client-Side Only Auth: Don't rely solely on client-side auth checks
- Checking Auth on Every Component: Use layouts and route groups instead
- Exposing Sensitive Data: Never send sensitive data to client without auth check
- Hardcoded Redirects: Use dynamic redirects based on user intent
- Ignoring Middleware: Always use middleware for session refresh
- Not Handling Loading States: Show appropriate loading states during auth checks