Complete Guide to Supabase Authentication in Next.js 14
What you'll learn: How to implement production-ready authentication in Next.js 14 using Supabase, including OAuth providers, Row Level Security, and middleware protection. This is the same auth system I used in DemandAI.Pro.
Why Supabase for Authentication?
After building multiple SaaS platforms, I've found Supabase to be the best balance of features, security, and developer experience. Here's why:
Built-in Security
Row Level Security (RLS) policies protect data at the database level, not just in your application code.
PostgreSQL Power
Full PostgreSQL database with realtime subscriptions, not a limited auth-only service.
Great DX
TypeScript SDK, automatic type generation, and excellent Next.js integration out of the box.
Free Tier
Generous free tier perfect for side projects and MVPs before scaling to paid plans.
Step 1: Project Setup
First, install the required dependencies:
npm install @supabase/supabase-js @supabase/ssr
npm install -D @supabase/auth-helpers-nextjsStep 2: Environment Variables
Create a .env.local file:
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-keySecurity Note: Never expose your service role key in client-side code. Only use it in server-side API routes or Server Components.
Step 3: Create Supabase Client
Create 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!
)
}
Create lib/supabase/server.ts for Server Components:
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
cookieStore.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
cookieStore.set({ name, value: '', ...options })
},
},
}
)
}
Step 4: Authentication Middleware
Create middleware.ts to protect routes:
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
response.cookies.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
response.cookies.set({ name, value: '', ...options })
},
},
}
)
const {
data: { user },
} = await supabase.auth.getUser()
// Protect dashboard routes
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Redirect authenticated users away from login
if (user && request.nextUrl.pathname === '/login') {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
}
Step 5: Login Component
Create a simple email/password login:
'use client'
import { createClient } from '@/lib/supabase/client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
export default function LoginForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const router = useRouter()
const supabase = createClient()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
alert(error.message)
} else {
router.push('/dashboard')
router.refresh()
}
setLoading(false)
}
return (
<form onSubmit={handleLogin} className="space-y-4">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
className="w-full px-4 py-2 rounded-lg bg-slate-800 text-white"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="w-full px-4 py-2 rounded-lg bg-slate-800 text-white"
required
/>
<button
type="submit"
disabled={loading}
className="w-full px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold disabled:opacity-50"
>
{loading ? 'Loading...' : 'Sign In'}
</button>
</form>
)
}
Step 6: OAuth Integration
Add social login (Google example):
const handleGoogleLogin = async () => {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
})
if (error) {
console.error('Error:', error.message)
}
}
<button
onClick={handleGoogleLogin}
className="w-full px-4 py-2 rounded-lg bg-white text-gray-900 font-semibold hover:bg-gray-100"
>
Continue with Google
</button>
Step 7: Row Level Security (RLS)
This is where Supabase really shines. Set up RLS policies in your database:
-- Enable RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only see their own projects
CREATE POLICY "Users can view own projects"
ON projects FOR SELECT
USING (auth.uid() = user_id);
-- Policy: Users can insert their own projects
CREATE POLICY "Users can insert own projects"
ON projects FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Policy: Users can update their own projects
CREATE POLICY "Users can update own projects"
ON projects FOR UPDATE
USING (auth.uid() = user_id);
Production Best Practices
Use Server Components for Initial Data
Fetch user data and protected content in Server Components to avoid auth checks on the client.
Always Enable RLS
Never rely solely on client-side auth. RLS policies protect your data at the database level.
Handle Token Refresh
Supabase handles token refresh automatically, but always check user state before protected operations.
Use Middleware for Route Protection
Protect routes at the middleware level for better performance and security.
Real-World Example
This is the exact authentication setup I used in DemandAI.Pro, where we needed:
- HIPAA-compliant data isolation (RLS handles this)
- Multiple OAuth providers for easy attorney onboarding
- Email verification for account security
- Session management across multiple devices
- Role-based access control for different user types
Supabase handled all of this out of the box, letting us focus on building the AI features instead of authentication infrastructure.
Conclusion
Supabase provides enterprise-grade authentication without the complexity of building your own auth system. The combination of PostgreSQL, RLS, and OAuth support makes it perfect for modern SaaS applications.
Need Help Implementing This?
I help startups and enterprises build production-ready SaaS platforms with Supabase, Next.js, and AI integration.
Get in Touch