Skip to main content
Back to Blog
DevelopmentTutorial

Complete Guide to Supabase Authentication in Next.js 14

December 19, 2024
12 min read
Ross Day

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-nextjs

Step 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-key

Security 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
RD
Ross Day
Full-Stack Developer & AI Integration Consultant
Building production SaaS platforms with Next.js, Supabase, and AI