Secure Your Next.js App with Supabase Auth – Complete Integration Guide - Techvblogs

Secure Your Next.js App with Supabase Auth – Complete Integration Guide

Learn how to set up Supabase Auth in Next.js for secure, scalable logins.


Suresh Ramani - Author - Techvblogs
Suresh Ramani
 

3 days ago

TechvBlogs - Google News

Building secure authentication into modern web applications doesn’t have to be complex or time-consuming. With Supabase Auth and Next.js, you can implement enterprise-grade user authentication that scales with your application while maintaining security best practices. This comprehensive guide will walk you through every step of integrating Supabase authentication into your Next.js application, from basic setup to advanced security configurations.

Why Authentication Matters in Modern Web Apps

User authentication serves as the foundation of application security, controlling access to sensitive data and personalized experiences. Poor authentication implementations can lead to data breaches, unauthorized access, and compliance violations that damage both user trust and business reputation.

Modern applications require authentication solutions that balance security with user experience. Users expect seamless login experiences across devices while maintaining confidence that their data remains protected. Additionally, developers need authentication systems that integrate smoothly with existing technology stacks without introducing unnecessary complexity.

What Makes Supabase Auth a Great Choice for Next.js Projects

Supabase Auth provides a complete authentication solution that eliminates the need to build user management systems from scratch. Unlike traditional authentication libraries that require extensive configuration and maintenance, Supabase handles the heavy lifting while providing extensive customization options.

Key advantages of Supabase Auth include:

  • Multiple Authentication Methods: Email/password, OAuth providers, magic links, and phone authentication
  • Automatic Security Features: JWT token management, session handling, and refresh token rotation
  • Built-in User Management: User profiles, email verification, and password recovery
  • Row Level Security: Database-level access controls that work seamlessly with authentication
  • Real-time Capabilities: Instant authentication state updates across application instances

The integration with Next.js is particularly powerful because both technologies prioritize developer experience and performance optimization.

Getting Started with the Stack

Overview of Supabase and Its Authentication Features

Supabase is an open-source Backend-as-a-Service platform built on PostgreSQL that provides authentication, real-time subscriptions, and storage capabilities. The authentication system is built on proven technologies including JWT tokens and PostgreSQL’s Row Level Security.

Core authentication features include:

  • User Registration and Login: Multiple authentication methods with customizable flows
  • Session Management: Automatic token refresh and secure session storage
  • Email Services: Built-in email templates for verification and password recovery
  • OAuth Integration: Support for major providers like Google, GitHub, Discord, and more
  • Security Policies: Fine-grained access controls at the database level

Why Choose Next.js for Building Secure Web Applications

Next.js provides the perfect foundation for secure applications through its comprehensive feature set:

  • Server-Side Rendering: Improved security through server-side session validation
  • API Routes: Secure backend endpoints within the same codebase
  • Middleware Support: Request interception for authentication checks
  • Environment Variable Management: Secure configuration handling
  • Production Optimizations: Built-in security headers and performance enhancements

The combination creates a full-stack solution where authentication flows seamlessly between client and server components.

Setting Up the Development Environment

Installing Next.js and Creating a New Project

Begin by creating a new Next.js project with TypeScript support for better type safety:

npx create-next-app@latest my-auth-app --typescript --tailwind --eslint --app
cd my-auth-app

This command creates a modern Next.js application with essential developer tools pre-configured.

Setting Up Supabase: Creating a Project and Getting API Keys

Visit the Supabase dashboard and create a new project. Once your project is ready, navigate to Settings > API to find your project credentials:

  • Project URL: Your unique Supabase project endpoint
  • Anon Public Key: Safe for client-side use with Row Level Security
  • Service Role Key: Server-side only, provides full database access

Store these credentials securely as you’ll need them for configuration.

Installing the Supabase JavaScript Client

Install the Supabase client library and additional dependencies:

npm install @supabase/supabase-js
npm install @supabase/auth-helpers-nextjs @supabase/auth-helpers-react

The auth helpers provide Next.js-specific utilities that streamline authentication integration.

Configuring Supabase in Your Next.js App

Creating a Supabase Client Instance

Create a utility file to initialize your Supabase client at lib/supabase.ts:

import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: true
  }
})

Using Environment Variables for Secure Configuration

Create a .env.local file in your project root:

NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key

The NEXT_PUBLIC_ prefix makes variables available in the browser, while server-only variables remain secure.

Initializing Supabase in a Utility File

Create a more robust client configuration that handles both client and server environments:

// lib/supabase-client.ts
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'

export const createClient = () => createClientComponentClient()

// lib/supabase-server.ts
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'

export const createServerClient = () => 
  createServerComponentClient({ cookies })

This separation ensures optimal performance and security for different execution contexts.

Understanding Supabase Auth Options

Supported Authentication Methods: Email, OAuth, Magic Links

Supabase supports multiple authentication strategies to accommodate different user preferences and security requirements:

Email/Password Authentication:

  • Traditional registration with email verification
  • Customizable password requirements
  • Built-in password recovery flows

OAuth Providers:

  • Google, GitHub, Discord, Twitter, and more
  • Simplified user onboarding
  • Access to user profile information

Magic Link Authentication:

  • Passwordless authentication via email
  • Enhanced security with time-limited links
  • Improved user experience for occasional users

Phone Authentication:

  • SMS-based verification
  • Global phone number support
  • Two-factor authentication capabilities

When to Use Which Auth Strategy

Choose authentication methods based on your application’s requirements:

  • Email/Password: Best for applications requiring user-created credentials and high security
  • OAuth: Ideal for applications targeting users with existing social media accounts
  • Magic Links: Perfect for applications prioritizing user experience over frequent access
  • Phone Auth: Essential for applications requiring identity verification or global accessibility

Enabling Auth Providers from the Supabase Dashboard

Configure authentication providers in your Supabase dashboard:

  1. Navigate to Authentication > Providers
  2. Enable desired providers (email is enabled by default)
  3. Configure OAuth applications with provider credentials
  4. Set up redirect URLs for each environment
  5. Customize email templates for branded communications

Building the User Authentication Flow

Creating a Signup Form with Email and Password

Build a comprehensive signup form with proper validation:

// components/SignupForm.tsx
'use client'
import { useState } from 'react'
import { supabase } from '@/lib/supabase'
import { useRouter } from 'next/navigation'

export function SignupForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const router = useRouter()

  const handleSignup = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setError(null)

    const { data, error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        emailRedirectTo: `${window.location.origin}/auth/callback`
      }
    })

    if (error) {
      setError(error.message)
    } else if (data.user && !data.user.email_confirmed_at) {
      setError('Please check your email for a verification link.')
    } else {
      router.push('/dashboard')
    }
    setLoading(false)
  }

  return (
    <form onSubmit={handleSignup} className="space-y-4 max-w-md mx-auto">
      <div>
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          Email Address
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          placeholder="[email protected]"
        />
      </div>
      
      <div>
        <label htmlFor="password" className="block text-sm font-medium text-gray-700">
          Password
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
          minLength={6}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          placeholder="Minimum 6 characters"
        />
      </div>

      {error && (
        <div className="text-red-600 text-sm bg-red-50 p-3 rounded-md">
          {error}
        </div>
      )}

      <button
        type="submit"
        disabled={loading}
        className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
      >
        {loading ? 'Creating Account...' : 'Create Account'}
      </button>
    </form>
  )
}

Implementing a Login Form with Real-Time Validation

Create a login form with enhanced user experience features:

// components/LoginForm.tsx
'use client'
import { useState, useEffect } from 'react'
import { supabase } from '@/lib/supabase'
import { useRouter } from 'next/navigation'

export function LoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [emailError, setEmailError] = useState('')
  const router = useRouter()

  // Real-time email validation
  useEffect(() => {
    if (email && !email.includes('@')) {
      setEmailError('Please enter a valid email address')
    } else {
      setEmailError('')
    }
  }, [email])

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setError(null)

    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password
    })

    if (error) {
      setError(error.message)
    } else {
      router.push('/dashboard')
    }
    setLoading(false)
  }

  return (
    <form onSubmit={handleLogin} className="space-y-4 max-w-md mx-auto">
      <div>
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          Email Address
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          className={`mt-1 block w-full rounded-md shadow-sm ${
            emailError ? 'border-red-300 focus:border-red-500 focus:ring-red-500' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
          }`}
          placeholder="[email protected]"
        />
        {emailError && (
          <p className="mt-1 text-sm text-red-600">{emailError}</p>
        )}
      </div>
      
      <div>
        <label htmlFor="password" className="block text-sm font-medium text-gray-700">
          Password
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
        />
      </div>

      {error && (
        <div className="text-red-600 text-sm bg-red-50 p-3 rounded-md">
          {error}
        </div>
      )}

      <button
        type="submit"
        disabled={loading || !!emailError}
        className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
      >
        {loading ? 'Signing In...' : 'Sign In'}
      </button>

      <div className="text-center">
        <a href="/auth/reset-password" className="text-sm text-blue-600 hover:text-blue-500">
          Forgot your password?
        </a>
      </div>
    </form>
  )
}

Handling Errors and Edge Cases Gracefully

Implement comprehensive error handling for common authentication scenarios:

// utils/authErrors.ts
export function getAuthErrorMessage(error: any): string {
  switch (error.message) {
    case 'Invalid login credentials':
      return 'The email or password you entered is incorrect. Please try again.'
    case 'Email not confirmed':
      return 'Please check your email and click the confirmation link before signing in.'
    case 'Signup disabled':
      return 'New account registration is currently disabled. Please contact support.'
    case 'Email rate limit exceeded':
      return 'Too many emails sent. Please wait before requesting another.'
    default:
      return error.message || 'An unexpected error occurred. Please try again.'
  }
}

// Usage in components
if (error) {
  setError(getAuthErrorMessage(error))
}

Managing Sessions in Next.js

Understanding Supabase Session Management

Supabase automatically handles session management through JWT tokens stored in browser cookies. Sessions include:

  • Access Token: Short-lived token for API authentication (1 hour default)
  • Refresh Token: Long-lived token for obtaining new access tokens
  • User Metadata: Basic user information and custom attributes

Storing and Retrieving the User Session

Create a global auth context for session management:

// contexts/AuthContext.tsx
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import { User, Session } from '@supabase/supabase-js'
import { supabase } from '@/lib/supabase'

interface AuthContextType {
  user: User | null
  session: Session | null
  loading: boolean
  signOut: () => Promise<void>
}

const AuthContext = createContext<AuthContextType>({
  user: null,
  session: null,
  loading: true,
  signOut: async () => {}
})

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  const [session, setSession] = useState<Session | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // Get initial session
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session)
      setUser(session?.user ?? null)
      setLoading(false)
    })

    // Listen for auth changes
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        setSession(session)
        setUser(session?.user ?? null)
        setLoading(false)

        // Handle specific auth events
        if (event === 'SIGNED_OUT') {
          setUser(null)
          setSession(null)
        }
      }
    )

    return () => subscription.unsubscribe()
  }, [])

  const signOut = async () => {
    await supabase.auth.signOut()
  }

  return (
    <AuthContext.Provider value={{ user, session, loading, signOut }}>
      {children}
    </AuthContext.Provider>
  )
}

export const useAuth = () => {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}

Refreshing Sessions Automatically

Supabase handles automatic token refresh, but you can customize the behavior:

// lib/supabase.ts
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: true,
    flowType: 'pkce' // Enhanced security for public clients
  }
})

// Custom session refresh handling
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'TOKEN_REFRESHED') {
    console.log('Session refreshed successfully')
  }
  if (event === 'TOKEN_REFRESH_FAILED') {
    console.error('Session refresh failed, redirecting to login')
    window.location.href = '/auth/login'
  }
})

Protecting Routes with Middleware and Guards

Creating Protected Pages in Next.js

Implement route protection at the page level:

// components/ProtectedRoute.tsx
'use client'
import { useAuth } from '@/contexts/AuthContext'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

interface ProtectedRouteProps {
  children: React.ReactNode
  redirectTo?: string
}

export function ProtectedRoute({ children, redirectTo = '/auth/login' }: ProtectedRouteProps) {
  const { user, loading } = useAuth()
  const router = useRouter()

  useEffect(() => {
    if (!loading && !user) {
      router.push(redirectTo)
    }
  }, [user, loading, router, redirectTo])

  if (loading) {
    return (
      <div className="flex justify-center items-center min-h-screen">
        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
      </div>
    )
  }

  if (!user) {
    return null // Redirect is happening
  }

  return <>{children}</>
}

// Usage in pages
export default function Dashboard() {
  return (
    <ProtectedRoute>
      <div>
        <h1>Dashboard</h1>
        <p>This content is only visible to authenticated users.</p>
      </div>
    </ProtectedRoute>
  )
}

Using getServerSideProps to Check for Auth Status

For server-side authentication checks:

// pages/dashboard.tsx (Pages Router)
import { GetServerSideProps } from 'next'
import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs'

export default function Dashboard({ user }: { user: any }) {
  return (
    <div>
      <h1>Welcome, {user.email}!</h1>
    </div>
  )
}

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const supabase = createServerSupabaseClient(ctx)
  const { data: { session } } = await supabase.auth.getSession()

  if (!session) {
    return {
      redirect: {
        destination: '/auth/login',
        permanent: false
      }
    }
  }

  return {
    props: {
      user: session.user
    }
  }
}

Redirecting Unauthorized Users

Create a middleware for global route protection:

// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(req: NextRequest) {
  const res = NextResponse.next()
  const supabase = createMiddlewareClient({ req, res })

  const { data: { session } } = await supabase.auth.getSession()

  // Protect dashboard routes
  if (req.nextUrl.pathname.startsWith('/dashboard')) {
    if (!session) {
      return NextResponse.redirect(new URL('/auth/login', req.url))
    }
  }

  // Redirect authenticated users away from auth pages
  if (req.nextUrl.pathname.startsWith('/auth') && session) {
    return NextResponse.redirect(new URL('/dashboard', req.url))
  }

  return res
}

export const config = {
  matcher: ['/dashboard/:path*', '/auth/:path*']
}

Adding OAuth Providers to Your App

Enabling Google, GitHub, or Twitter Authentication

Configure OAuth providers in your Supabase dashboard and implement the frontend:

// components/OAuthButtons.tsx
'use client'
import { supabase } from '@/lib/supabase'

export function OAuthButtons() {
  const handleOAuthSignIn = async (provider: 'google' | 'github' | 'twitter') => {
    const { error } = await supabase.auth.signInWithOAuth({
      provider,
      options: {
        redirectTo: `${window.location.origin}/auth/callback`
      }
    })

    if (error) {
      console.error('OAuth error:', error.message)
    }
  }

  return (
    <div className="space-y-3">
      <button
        onClick={() => handleOAuthSignIn('google')}
        className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50"
      >
        <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
          {/* Google icon SVG path */}
        </svg>
        Continue with Google
      </button>

      <button
        onClick={() => handleOAuthSignIn('github')}
        className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50"
      >
        <svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
          {/* GitHub icon SVG path */}
        </svg>
        Continue with GitHub
      </button>
    </div>
  )
}

Implementing OAuth Logins in Your Next.js App

Create a callback handler for OAuth redirects:

// app/auth/callback/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const requestUrl = new URL(request.url)
  const code = requestUrl.searchParams.get('code')

  if (code) {
    const supabase = createRouteHandlerClient({ cookies })
    
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    
    if (error) {
      return NextResponse.redirect(`${requestUrl.origin}/auth/error?message=${error.message}`)
    }
  }

  return NextResponse.redirect(`${requestUrl.origin}/dashboard`)
}

Customizing the OAuth Login Flow

Add custom scopes and options for OAuth providers:

const handleGoogleSignIn = async () => {
  const { error } = await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      scopes: 'profile email',
      queryParams: {
        access_type: 'offline',
        prompt: 'consent'
      },
      redirectTo: `${window.location.origin}/auth/callback`
    }
  })
}

Handling Email Confirmation and Password Reset

Sending and Verifying Email Confirmations

Customize email confirmation flow:

// utils/emailConfirmation.ts
export async function resendConfirmationEmail(email: string) {
  const { error } = await supabase.auth.resend({
    type: 'signup',
    email,
    options: {
      emailRedirectTo: `${window.location.origin}/auth/callback`
    }
  })

  return { error }
}

// Component for email confirmation
export function EmailConfirmation({ email }: { email: string }) {
  const [loading, setLoading] = useState(false)
  const [message, setMessage] = useState('')

  const handleResend = async () => {
    setLoading(true)
    const { error } = await resendConfirmationEmail(email)
    
    if (error) {
      setMessage('Failed to send confirmation email. Please try again.')
    } else {
      setMessage('Confirmation email sent! Please check your inbox.')
    }
    setLoading(false)
  }

  return (
    <div className="text-center space-y-4">
      <h2 className="text-lg font-semibold">Check Your Email</h2>
      <p className="text-gray-600">
        We sent a confirmation link to <strong>{email}</strong>
      </p>
      <button
        onClick={handleResend}
        disabled={loading}
        className="text-blue-600 hover:text-blue-500 disabled:opacity-50"
      >
        {loading ? 'Sending...' : 'Resend confirmation email'}
      </button>
      {message && (
        <p className="text-sm text-gray-600">{message}</p>
      )}
    </div>
  )
}

Building Password Reset Request and Reset Pages

Create password reset functionality:

// components/PasswordResetRequest.tsx
'use client'
import { useState } from 'react'
import { supabase } from '@/lib/supabase'

export function PasswordResetRequest() {
  const [email, setEmail] = useState('')
  const [loading, setLoading] = useState(false)
  const [sent, setSent] = useState(false)

  const handlePasswordReset = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)

    const { error } = await supabase.auth.resetPasswordForEmail(email, {
      redirectTo: `${window.location.origin}/auth/reset-password`
    })

    if (!error) {
      setSent(true)
    }
    setLoading(false)
  }

  if (sent) {
    return (
      <div className="text-center space-y-4">
        <h2 className="text-lg font-semibold">Reset Link Sent</h2>
        <p className="text-gray-600">
          Check your email for a link to reset your password.
        </p>
      </div>
    )
  }

  return (
    <form onSubmit={handlePasswordReset} className="space-y-4 max-w-md mx-auto">
      <div>
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          Email Address
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          placeholder="Enter your email address"
        />
      </div>

      <button
        type="submit"
        disabled={loading}
        className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
      >
        {loading ? 'Sending...' : 'Send Reset Link'}
      </button>
    </form>
  )
}

// components/PasswordResetForm.tsx
'use client'
import { useState } from 'react'
import { supabase } from '@/lib/supabase'
import { useRouter } from 'next/navigation'

export function PasswordResetForm() {
  const [password, setPassword] = useState('')
  const [confirmPassword, setConfirmPassword] = useState('')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')
  const router = useRouter()

  const handlePasswordUpdate = async (e: React.FormEvent) => {
    e.preventDefault()
    setError('')

    if (password !== confirmPassword) {
      setError('Passwords do not match')
      return
    }

    if (password.length < 6) {
      setError('Password must be at least 6 characters')
      return
    }

    setLoading(true)

    const { error } = await supabase.auth.updateUser({
      password: password
    })

    if (error) {
      setError(error.message)
    } else {
      router.push('/dashboard')
    }
    setLoading(false)
  }

  return (
    <form onSubmit={handlePasswordUpdate} className="space-y-4 max-w-md mx-auto">
      <div>
        <label htmlFor="password" className="block text-sm font-medium text-gray-700">
          New Password
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
          minLength={6}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
        />
      </div>

      <div>
        <label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700">
          Confirm New Password
        </label>
        <input
          id="confirmPassword"
          type="password"
          value={confirmPassword}
          onChange={(e) => setConfirmPassword(e.target.value)}
          required
          minLength={6}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
        />
      </div>

      {error && (
        <div className="text-red-600 text-sm bg-red-50 p-3 rounded-md">
          {error}
        </div>
      )}

      <button
        type="submit"
        disabled={loading}
        className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
      >
        {loading ? 'Updating...' : 'Update Password'}
      </button>
    </form>
  )
}

Using Supabase API to Manage User Credentials

Implement additional credential management features:

// utils/userManagement.ts
export async function updateUserEmail(newEmail: string) {
  const { error } = await supabase.auth.updateUser({
    email: newEmail
  })
  return { error }
}

export async function updateUserMetadata(metadata: Record<string, any>) {
  const { error } = await supabase.auth.updateUser({
    data: metadata
  })
  return { error }
}

export async function deleteUserAccount() {
  // Note: This requires admin privileges or a database function
  const { data: user } = await supabase.auth.getUser()
  
  if (user.user) {
    // Delete user data first (following your data retention policies)
    await supabase.from('user_profiles').delete().eq('id', user.user.id)
    
    // Then sign out the user
    await supabase.auth.signOut()
  }
}

Implementing Magic Link Authentication

How Magic Links Work with Supabase

Magic links provide passwordless authentication by sending time-limited links via email. When users click these links, they’re automatically signed in without entering credentials. This approach enhances security by eliminating password-related vulnerabilities while improving user experience.

Creating a Login Form with Magic Link Support

Build a dual-purpose form supporting both password and magic link authentication:

// components/MagicLinkForm.tsx
'use client'
import { useState } from 'react'
import { supabase } from '@/lib/supabase'

export function MagicLinkForm() {
  const [email, setEmail] = useState('')
  const [loading, setLoading] = useState(false)
  const [sent, setSent] = useState(false)
  const [error, setError] = useState('')

  const handleMagicLink = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setError('')

    const { error } = await supabase.auth.signInWithOtp({
      email,
      options: {
        emailRedirectTo: `${window.location.origin}/auth/callback`,
        shouldCreateUser: true
      }
    })

    if (error) {
      setError(error.message)
    } else {
      setSent(true)
    }
    setLoading(false)
  }

  if (sent) {
    return (
      <div className="text-center space-y-4">
        <div className="w-12 h-12 mx-auto bg-green-100 rounded-full flex items-center justify-center">
          <svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
          </svg>
        </div>
        <h2 className="text-lg font-semibold">Check Your Email</h2>
        <p className="text-gray-600">
          We sent a magic link to <strong>{email}</strong>
        </p>
        <p className="text-sm text-gray-500">
          Click the link in the email to sign in. The link will expire in 1 hour.
        </p>
        <button
          onClick={() => setSent(false)}
          className="text-blue-600 hover:text-blue-500 text-sm"
        >
          Use a different email
        </button>
      </div>
    )
  }

  return (
    <form onSubmit={handleMagicLink} className="space-y-4 max-w-md mx-auto">
      <div>
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          Email Address
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          placeholder="Enter your email for a magic link"
        />
      </div>

      {error && (
        <div className="text-red-600 text-sm bg-red-50 p-3 rounded-md">
          {error}
        </div>
      )}

      <button
        type="submit"
        disabled={loading}
        className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 transition-colors"
      >
        {loading ? 'Sending Magic Link...' : 'Send Magic Link'}
      </button>

      <div className="text-center">
        <span className="text-gray-500 text-sm">
          No password required • Secure & simple
        </span>
      </div>
    </form>
  )
}

Handling Redirects and Session Restoration

Create a comprehensive callback handler for magic links:

// app/auth/callback/page.tsx
'use client'
import { useEffect, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { supabase } from '@/lib/supabase'

export default function AuthCallback() {
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const router = useRouter()
  const searchParams = useSearchParams()

  useEffect(() => {
    const handleAuthCallback = async () => {
      try {
        const code = searchParams.get('code')
        const errorParam = searchParams.get('error')
        const errorDescription = searchParams.get('error_description')

        if (errorParam) {
          setError(errorDescription || 'Authentication failed')
          setLoading(false)
          return
        }

        if (code) {
          const { error } = await supabase.auth.exchangeCodeForSession(code)
          
          if (error) {
            setError(error.message)
          } else {
            // Successful authentication
            router.push('/dashboard')
            return
          }
        }
      } catch (err) {
        setError('An unexpected error occurred')
      }
      
      setLoading(false)
    }

    handleAuthCallback()
  }, [searchParams, router])

  if (loading) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="text-center space-y-4">
          <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
          <p className="text-gray-600">Completing authentication...</p>
        </div>
      </div>
    )
  }

  if (error) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="text-center space-y-4 max-w-md">
          <div className="w-12 h-12 mx-auto bg-red-100 rounded-full flex items-center justify-center">
            <svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
            </svg>
          </div>
          <h2 className="text-lg font-semibold text-gray-900">Authentication Failed</h2>
          <p className="text-gray-600">{error}</p>
          <button
            onClick={() => router.push('/auth/login')}
            className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
          >
            Try Again
          </button>
        </div>
      </div>
    )
  }

  return null
}

Securing API Routes with Supabase Auth

Creating API Routes in Next.js

Build secure API endpoints that verify authentication:

// app/api/posts/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export async function GET() {
  const supabase = createRouteHandlerClient({ cookies })
  
  // Verify authentication
  const { data: { session }, error: authError } = await supabase.auth.getSession()
  
  if (authError || !session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // Fetch user-specific data
  const { data: posts, error } = await supabase
    .from('posts')
    .select('*')
    .eq('user_id', session.user.id)
    .order('created_at', { ascending: false })

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ posts })
}

export async function POST(request: Request) {
  const supabase = createRouteHandlerClient({ cookies })
  
  const { data: { session }, error: authError } = await supabase.auth.getSession()
  
  if (authError || !session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  try {
    const body = await request.json()
    const { title, content } = body

    // Validate input
    if (!title || title.trim().length === 0) {
      return NextResponse.json({ error: 'Title is required' }, { status: 400 })
    }

    // Create post with user ID
    const { data: post, error } = await supabase
      .from('posts')
      .insert([
        {
          title: title.trim(),
          content: content?.trim() || '',
          user_id: session.user.id
        }
      ])
      .select()
      .single()

    if (error) {
      return NextResponse.json({ error: error.message }, { status: 500 })
    }

    return NextResponse.json({ post }, { status: 201 })
  } catch (error) {
    return NextResponse.json({ error: 'Invalid request body' }, { status: 400 })
  }
}

Verifying JWT Tokens in API Handlers

Implement custom JWT verification for additional security:

// utils/auth.ts
import { jwtVerify } from 'jose'

export async function verifyJWT(token: string) {
  try {
    const secret = new TextEncoder().encode(process.env.SUPABASE_JWT_SECRET)
    const { payload } = await jwtVerify(token, secret)
    return { payload, error: null }
  } catch (error) {
    return { payload: null, error: 'Invalid token' }
  }
}

// API route with manual JWT verification
export async function GET(request: Request) {
  const authHeader = request.headers.get('authorization')
  const token = authHeader?.replace('Bearer ', '')

  if (!token) {
    return NextResponse.json({ error: 'Missing authorization header' }, { status: 401 })
  }

  const { payload, error } = await verifyJWT(token)
  
  if (error || !payload) {
    return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
  }

  // Use payload.sub as user ID
  const userId = payload.sub
  
  // Proceed with authenticated request...
}

Allowing Only Authenticated Users to Access API Data

Create middleware for API route protection:

// utils/apiAuth.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export async function withAuth(
  handler: (request: Request, context: { user: any }) => Promise<NextResponse>
) {
  return async (request: Request) => {
    const supabase = createRouteHandlerClient({ cookies })
    
    const { data: { session }, error } = await supabase.auth.getSession()
    
    if (error || !session) {
      return NextResponse.json(
        { error: 'Authentication required' }, 
        { status: 401 }
      )
    }

    return handler(request, { user: session.user })
  }
}

// Usage in API routes
export const GET = withAuth(async (request, { user }) => {
  // This handler only runs for authenticated users
  return NextResponse.json({ message: `Hello ${user.email}` })
})

Creating a Custom User Dashboard

Displaying User Profile Information

Build a comprehensive user dashboard:

// components/UserDashboard.tsx
'use client'
import { useState, useEffect } from 'react'
import { useAuth } from '@/contexts/AuthContext'
import { supabase } from '@/lib/supabase'

interface UserProfile {
  id: string
  full_name: string | null
  avatar_url: string | null
  bio: string | null
  website: string | null
  updated_at: string
}

export function UserDashboard() {
  const { user } = useAuth()
  const [profile, setProfile] = useState<UserProfile | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    if (user) {
      fetchProfile()
    }
  }, [user])

  const fetchProfile = async () => {
    if (!user) return

    const { data, error } = await supabase
      .from('profiles')
      .select('*')
      .eq('id', user.id)
      .single()

    if (data) {
      setProfile(data)
    } else if (error && error.code === 'PGRST116') {
      // Profile doesn't exist, create one
      await createProfile()
    }
    setLoading(false)
  }

  const createProfile = async () => {
    if (!user) return

    const { data, error } = await supabase
      .from('profiles')
      .insert([
        {
          id: user.id,
          full_name: user.user_metadata?.full_name || '',
          avatar_url: user.user_metadata?.avatar_url || null
        }
      ])
      .select()
      .single()

    if (data) {
      setProfile(data)
    }
  }

  if (loading) {
    return (
      <div className="flex justify-center items-center p-8">
        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
      </div>
    )
  }

  return (
    <div className="max-w-4xl mx-auto p-6">
      <div className="bg-white rounded-lg shadow-md p-6 mb-6">
        <div className="flex items-center space-x-4">
          <div className="w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center">
            {profile?.avatar_url ? (
              <img
                src={profile.avatar_url}
                alt="Profile"
                className="w-16 h-16 rounded-full object-cover"
              />
            ) : (
              <span className="text-xl font-semibold text-gray-600">
                {user?.email?.charAt(0).toUpperCase()}
              </span>
            )}
          </div>
          <div>
            <h1 className="text-2xl font-bold text-gray-900">
              {profile?.full_name || 'Welcome!'}
            </h1>
            <p className="text-gray-600">{user?.email}</p>
            <p className="text-sm text-gray-500">
              Member since {new Date(user?.created_at || '').toLocaleDateString()}
            </p>
          </div>
        </div>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
        <div className="bg-white rounded-lg shadow-md p-6">
          <h2 className="text-lg font-semibold mb-4">Account Information</h2>
          <div className="space-y-3">
            <div>
              <label className="text-sm font-medium text-gray-700">Email</label>
              <p className="text-gray-900">{user?.email}</p>
            </div>
            <div>
              <label className="text-sm font-medium text-gray-700">Email Verified</label>
              <p className="text-gray-900">
                {user?.email_confirmed_at ? (
                  <span className="text-green-600">✓ Verified</span>
                ) : (
                  <span className="text-orange-600">⚠ Unverified</span>
                )}
              </p>
            </div>
            <div>
              <label className="text-sm font-medium text-gray-700">Last Sign In</label>
              <p className="text-gray-900">
                {user?.last_sign_in_at ? 
                  new Date(user.last_sign_in_at).toLocaleString() : 
                  'Never'
                }
              </p>
            </div>
          </div>
        </div>

        <div className="bg-white rounded-lg shadow-md p-6">
          <h2 className="text-lg font-semibold mb-4">Profile Details</h2>
          <div className="space-y-3">
            <div>
              <label className="text-sm font-medium text-gray-700">Full Name</label>
              <p className="text-gray-900">{profile?.full_name || 'Not set'}</p>
            </div>
            <div>
              <label className="text-sm font-medium text-gray-700">Bio</label>
              <p className="text-gray-900">{profile?.bio || 'No bio added'}</p>
            </div>
            <div>
              <label className="text-sm font-medium text-gray-700">Website</label>
              <p className="text-gray-900">
                {profile?.website ? (
                  <a href={profile.website} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
                    {profile.website}
                  </a>
                ) : (
                  'No website'
                )}
              </p>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

Allowing Users to Update Their Profile Data

Create an editable profile form:

// components/ProfileEditor.tsx
'use client'
import { useState } from 'react'
import { useAuth } from '@/contexts/AuthContext'
import { supabase } from '@/lib/supabase'

interface ProfileData {
  full_name: string
  bio: string
  website: string
}

export function ProfileEditor({ profile, onUpdate }: { 
  profile: any
  onUpdate: () => void 
}) {
  const { user } = useAuth()
  const [formData, setFormData] = useState<ProfileData>({
    full_name: profile?.full_name || '',
    bio: profile?.bio || '',
    website: profile?.website || ''
  })
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')
  const [success, setSuccess] = useState(false)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!user) return

    setLoading(true)
    setError('')
    setSuccess(false)

    const { error } = await supabase
      .from('profiles')
      .upsert({
        id: user.id,
        ...formData,
        updated_at: new Date().toISOString()
      })

    if (error) {
      setError(error.message)
    } else {
      setSuccess(true)
      onUpdate()
      setTimeout(() => setSuccess(false), 3000)
    }
    setLoading(false)
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="full_name" className="block text-sm font-medium text-gray-700">
          Full Name
        </label>
        <input
          id="full_name"
          type="text"
          value={formData.full_name}
          onChange={(e) => setFormData({ ...formData, full_name: e.target.value })}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          placeholder="Your full name"
        />
      </div>

      <div>
        <label htmlFor="bio" className="block text-sm font-medium text-gray-700">
          Bio
        </label>
        <textarea
          id="bio"
          value={formData.bio}
          onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
          rows={3}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          placeholder="Tell us about yourself"
        />
      </div>

      <div>
        <label htmlFor="website" className="block text-sm font-medium text-gray-700">
          Website
        </label>
        <input
          id="website"
          type="url"
          value={formData.website}
          onChange={(e) => setFormData({ ...formData, website: e.target.value })}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          placeholder="https://yourwebsite.com"
        />
      </div>

      {error && (
        <div className="text-red-600 text-sm bg-red-50 p-3 rounded-md">
          {error}
        </div>
      )}

      {success && (
        <div className="text-green-600 text-sm bg-green-50 p-3 rounded-md">
          Profile updated successfully!
        </div>
      )}

      <button
        type="submit"
        disabled={loading}
        className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 transition-colors"
      >
        {loading ? 'Updating...' : 'Update Profile'}
      </button>
    </form>
  )
}

Securing Profile Updates with Supabase Policies

Create database policies to secure profile updates:

-- Create profiles table
CREATE TABLE profiles (
  id UUID REFERENCES auth.users(id) PRIMARY KEY,
  full_name TEXT,
  bio TEXT,
  website TEXT,
  avatar_url TEXT,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Enable RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

-- Policy for users to read their own profile
CREATE POLICY "Users can view own profile" ON profiles
  FOR SELECT USING (auth.uid() = id);

-- Policy for users to update their own profile
CREATE POLICY "Users can update own profile" ON profiles
  FOR UPDATE USING (auth.uid() = id);

-- Policy for users to insert their own profile
CREATE POLICY "Users can insert own profile" ON profiles
  FOR INSERT WITH CHECK (auth.uid() = id);

-- Function to automatically create profile on user signup
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.profiles (id, full_name, avatar_url)
  VALUES (
    NEW.id,
    NEW.raw_user_meta_data->>'full_name',
    NEW.raw_user_meta_data->>'avatar_url'
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Trigger to call the function on new user creation
CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();

Using Row Level Security (RLS) for Data Protection

What Is RLS and Why It’s Important

Row Level Security is a PostgreSQL feature that automatically filters database queries based on the current user’s identity. Unlike application-level security that can be bypassed, RLS enforcement happens at the database level, providing an additional security layer.

Benefits of RLS include:

  • Automatic Query Filtering: Users only see data they’re authorized to access
  • Defense in Depth: Security enforced even if application logic fails
  • Simplified Development: No need to add WHERE clauses to every query
  • Audit Compliance: Built-in access controls for sensitive data

Creating Policies to Restrict Access Based on Auth User

Implement comprehensive RLS policies for a blog application:

-- Posts table with RLS
CREATE TABLE posts (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  title TEXT NOT NULL,
  content TEXT,
  published BOOLEAN DEFAULT false,
  author_id UUID REFERENCES auth.users(id) NOT NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Authors can read, update, and delete their own posts
CREATE POLICY "Authors can manage own posts" ON posts
  USING (auth.uid() = author_id);

-- Everyone can read published posts
CREATE POLICY "Public can read published posts" ON posts
  FOR SELECT USING (published = true);

-- Comments table with RLS
CREATE TABLE comments (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
  author_id UUID REFERENCES auth.users(id),
  content TEXT NOT NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

ALTER TABLE comments ENABLE ROW LEVEL SECURITY;

-- Anyone can read comments on published posts
CREATE POLICY "Public can read comments on published posts" ON comments
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM posts 
      WHERE posts.id = comments.post_id 
      AND posts.published = true
    )
  );

-- Authenticated users can create comments
CREATE POLICY "Authenticated users can create comments" ON comments
  FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);

-- Authors can manage their own comments
CREATE POLICY "Authors can manage own comments" ON comments
  USING (auth.uid() = author_id);

Testing and Debugging RLS Rules in Supabase

Test RLS policies effectively using these approaches:

-- Test policies in SQL editor
-- First, simulate a user session
SELECT auth.jwt();

-- Test as a specific user (replace with actual user ID)
SELECT set_config('request.jwt.claims', '{"sub":"user-id-here"}', true);

-- Query data to verify policy enforcement
SELECT * FROM posts; -- Should only return user's posts or published posts

-- Reset session
SELECT set_config('request.jwt.claims', NULL, true);

-- Create a debugging function
CREATE OR REPLACE FUNCTION debug_rls_policies(table_name TEXT)
RETURNS TABLE(policy_name TEXT, policy_cmd TEXT, policy_quals TEXT) AS $$
BEGIN
  RETURN QUERY
  SELECT 
    pol.polname::TEXT,
    pol.polcmd::TEXT,
    pg_get_expr(pol.polqual, pol.polrelid)::TEXT
  FROM pg_policy pol
  JOIN pg_class pc ON pol.polrelid = pc.oid
  WHERE pc.relname = table_name;
END;
$$ LANGUAGE plpgsql;

-- Use the debugging function
SELECT * FROM debug_rls_policies('posts');

Debug RLS issues in your application:

// utils/debugRLS.ts
export async function debugUserAccess() {
  const { data: { session } } = await supabase.auth.getSession()
  
  if (session) {
    console.log('Current user:', session.user.id)
    console.log('JWT claims:', session.access_token)
    
    // Test query with explicit logging
    const { data, error, count } = await supabase
      .from('posts')
      .select('*', { count: 'exact' })
    
    console.log('Query results:', { data, error, count })
  }
}

Deploying Your Authenticated App

Deploying to Vercel with Environment Variables

Deploy your application to Vercel with proper configuration:

# Install Vercel CLI
npm install -g vercel

# Deploy the application
vercel

# Add environment variables
vercel env add NEXT_PUBLIC_SUPABASE_URL
vercel env add NEXT_PUBLIC_SUPABASE_ANON_KEY
vercel env add SUPABASE_SERVICE_ROLE_KEY

# Redeploy with new environment variables
vercel --prod

Configure your vercel.json for optimal deployment:

{
  "framework": "nextjs",
  "regions": ["iad1"],
  "env": {
    "NEXT_PUBLIC_SUPABASE_URL": "@supabase_url",
    "NEXT_PUBLIC_SUPABASE_ANON_KEY": "@supabase_anon_key"
  },
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "Referrer-Policy",
          "value": "strict-origin-when-cross-origin"
        }
      ]
    }
  ]
}

Setting Up Supabase Auth for Production

Configure your Supabase project for production deployment:

  1. Update Site URL: Set your production domain in Authentication > Settings
  2. Configure Redirect URLs: Add production callback URLs
  3. Enable Email Confirmations: Ensure email verification is enabled
  4. Set Up Custom SMTP: Configure branded email delivery
  5. Review Rate Limits: Adjust for production traffic patterns

Update your Supabase configuration:

// lib/supabase.ts - Production configuration
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: true,
    flowType: 'pkce',
    debug: process.env.NODE_ENV === 'development'
  },
  db: {
    schema: 'public'
  },
  global: {
    headers: {
      'X-Client-Info': `${process.env.npm_package_name}@${process.env.npm_package_version}`
    }
  }
})

Ensuring Secure Redirects and HTTPS

Implement secure redirect handling:

// utils/redirects.ts
export function getSecureRedirectUrl(url: string): string {
  const allowedDomains = [
    process.env.NEXT_PUBLIC_SITE_URL,
    'localhost:3000' // Development only
  ]

  try {
    const parsedUrl = new URL(url)
    const isAllowed = allowedDomains.some(domain => 
      parsedUrl.origin.includes(domain || '')
    )

    if (isAllowed) {
      return url
    }
  } catch {
    // Invalid URL
  }

  // Fallback to safe default
  return process.env.NEXT_PUBLIC_SITE_URL || '/'
}

// Usage in authentication flows
const { error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: getSecureRedirectUrl(callbackUrl)
  }
})

Advanced Security Tips and Best Practices

Using Supabase Hooks for Custom Auth Logic

Implement custom authentication logic using database functions and triggers:

-- Create a function to handle custom user creation logic
CREATE OR REPLACE FUNCTION public.handle_new_user_advanced()
RETURNS TRIGGER AS $$
BEGIN
  -- Create user profile
  INSERT INTO public.profiles (id, full_name, avatar_url, role)
  VALUES (
    NEW.id,
    NEW.raw_user_meta_data->>'full_name',
    NEW.raw_user_meta_data->>'avatar_url',
    COALESCE(NEW.raw_user_meta_data->>'role', 'user')
  );

  -- Create user settings with defaults
  INSERT INTO public.user_settings (user_id, email_notifications, theme)
  VALUES (NEW.id, true, 'light');

  -- Log the user creation event
  INSERT INTO public.audit_logs (user_id, action, details)
  VALUES (NEW.id, 'user_created', jsonb_build_object('email', NEW.email));

  -- Send welcome email (could trigger external service)
  INSERT INTO public.email_queue (user_id, template, recipient)
  VALUES (NEW.id, 'welcome', NEW.email);

  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Function to handle user deletions
CREATE OR REPLACE FUNCTION public.handle_user_delete()
RETURNS TRIGGER AS $$
BEGIN
  -- Anonymize user data instead of hard delete
  UPDATE public.profiles 
  SET 
    full_name = 'Deleted User',
    bio = NULL,
    avatar_url = NULL,
    deleted_at = NOW()
  WHERE id = OLD.id;

  -- Log the deletion
  INSERT INTO public.audit_logs (user_id, action, details)
  VALUES (OLD.id, 'user_deleted', jsonb_build_object('email', OLD.email));

  RETURN OLD;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Create triggers
CREATE TRIGGER on_auth_user_created_advanced
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user_advanced();

CREATE TRIGGER on_auth_user_deleted
  BEFORE DELETE ON auth.users
  FOR EACH ROW EXECUTE PROCEDURE public.handle_user_delete();

Avoiding Common Auth Mistakes

Prevent common authentication vulnerabilities:

// utils/authSecurity.ts
export class AuthSecurity {
  // Validate session on sensitive operations
  static async validateSessionForSensitiveOperation(requiredRole?: string) {
    const { data: { session }, error } = await supabase.auth.getSession()
    
    if (error || !session) {
      throw new Error('Authentication required')
    }

    // Check session age for sensitive operations
    const sessionAge = Date.now() - new Date(session.created_at).getTime()
    const maxAge = 15 * 60 * 1000 // 15 minutes

    if (sessionAge > maxAge) {
      throw new Error('Session too old, please re-authenticate')
    }

    // Check user role if required
    if (requiredRole) {
      const userRole = session.user.user_metadata?.role
      if (userRole !== requiredRole && userRole !== 'admin') {
        throw new Error('Insufficient permissions')
      }
    }

    return session
  }

  // Rate limiting for auth attempts
  static async checkRateLimit(identifier: string, action: string) {
    const key = `${action}:${identifier}`
    const limit = action === 'login' ? 5 : 3 // Different limits for different actions
    const window = 15 * 60 * 1000 // 15 minutes

    // In production, use Redis or similar
    // For demo, using simple in-memory storage
    const attempts = this.getAttempts(key, window)
    
    if (attempts >= limit) {
      throw new Error(`Too many ${action} attempts. Please try again later.`)
    }

    this.recordAttempt(key)
  }

  private static attempts = new Map<string, number[]>()

  private static getAttempts(key: string, window: number): number {
    const now = Date.now()
    const attemptTimes = this.attempts.get(key) || []
    
    // Remove old attempts outside the window
    const recentAttempts = attemptTimes.filter(time => now - time < window)
    this.attempts.set(key, recentAttempts)
    
    return recentAttempts.length
  }

  private static recordAttempt(key: string) {
    const attempts = this.attempts.get(key) || []
    attempts.push(Date.now())
    this.attempts.set(key, attempts)
  }

  // Secure password validation
  static validatePassword(password: string): { isValid: boolean; errors: string[] } {
    const errors: string[] = []

    if (password.length < 8) {
      errors.push('Password must be at least 8 characters long')
    }

    if (!/[A-Z]/.test(password)) {
      errors.push('Password must contain at least one uppercase letter')
    }

    if (!/[a-z]/.test(password)) {
      errors.push('Password must contain at least one lowercase letter')
    }

    if (!/\d/.test(password)) {
      errors.push('Password must contain at least one number')
    }

    if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
      errors.push('Password must contain at least one special character')
    }

    // Check against common passwords
    const commonPasswords = ['password', '123456', 'qwerty', 'admin']
    if (commonPasswords.includes(password.toLowerCase())) {
      errors.push('Password is too common')
    }

    return {
      isValid: errors.length === 0,
      errors
    }
  }
}

// Enhanced login form with security features
export function SecureLoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')
  const [attempts, setAttempts] = useState(0)

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setError('')

    try {
      // Check rate limiting
      await AuthSecurity.checkRateLimit(email, 'login')

      const { error } = await supabase.auth.signInWithPassword({
        email,
        password
      })

      if (error) {
        setAttempts(prev => prev + 1)
        throw error
      }

      // Successful login - reset attempts
      setAttempts(0)
      
    } catch (err: any) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }

  return (
    <form onSubmit={handleLogin} className="space-y-4">
      {/* Form fields */}
      {attempts >= 3 && (
        <div className="bg-yellow-50 border border-yellow-200 rounded-md p-3">
          <p className="text-sm text-yellow-800">
            Multiple failed attempts detected. Consider using password reset if you've forgotten your password.
          </p>
        </div>
      )}
      {/* Rest of form */}
    </form>
  )
}

Logging Out and Revoking Sessions Properly

Implement comprehensive logout functionality:

// utils/logout.ts
export class LogoutManager {
  // Standard logout
  static async logout() {
    try {
      // Clear any local state first
      this.clearLocalData()

      // Sign out from Supabase
      const { error } = await supabase.auth.signOut()
      
      if (error) {
        console.error('Logout error:', error)
      }

      // Redirect to login page
      window.location.href = '/auth/login'
    } catch (error) {
      console.error('Logout failed:', error)
    }
  }

  // Logout from all sessions
  static async logoutEverywhere() {
    try {
      // Get current session to identify user
      const { data: { session } } = await supabase.auth.getSession()
      
      if (session) {
        // Call API to revoke all sessions for this user
        await fetch('/api/auth/revoke-all-sessions', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${session.access_token}`,
            'Content-Type': 'application/json'
          }
        })
      }

      // Clear local data and redirect
      this.clearLocalData()
      window.location.href = '/auth/login'
    } catch (error) {
      console.error('Global logout failed:', error)
    }
  }

  // Emergency logout (for security incidents)
  static async emergencyLogout() {
    // Immediately clear all local data
    this.clearLocalData()
    
    // Clear all storage
    localStorage.clear()
    sessionStorage.clear()
    
    // Clear cookies
    document.cookie.split(";").forEach(c => {
      document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/")
    })

    // Force reload to clear any cached auth state
    window.location.replace('/auth/login')
  }

  private static clearLocalData() {
    // Clear any application-specific data
    localStorage.removeItem('user-preferences')
    localStorage.removeItem('draft-posts')
    // Add other cleanup as needed
  }
}

// Logout component with options
export function LogoutButton() {
  const [showOptions, setShowOptions] = useState(false)
  const { signOut } = useAuth()

  return (
    <div className="relative">
      <button
        onClick={() => setShowOptions(!showOptions)}
        className="text-gray-700 hover:text-gray-900"
      >
        Sign Out
      </button>

      {showOptions && (
        <div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50">
          <button
            onClick={() => {
              LogoutManager.logout()
              setShowOptions(false)
            }}
            className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 w-full text-left"
          >
            Sign out
          </button>
          <button
            onClick={() => {
              LogoutManager.logoutEverywhere()
              setShowOptions(false)
            }}
            className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 w-full text-left"
          >
            Sign out everywhere
          </button>
        </div>
      )}
    </div>
  )
}

Troubleshooting Common Issues

Fixing Session Persistence Problems

Resolve common session persistence issues:

// utils/sessionDebug.ts
export class SessionDebugger {
  static async diagnoseSessionIssues() {
    console.group('🔍 Session Diagnostics')

    // Check if Supabase client is initialized
    console.log('1. Supabase client initialized:', !!supabase)

    // Check environment variables
    console.log('2. Environment variables:')
    console.log('   SUPABASE_URL:', !!process.env.NEXT_PUBLIC_SUPABASE_URL)
    console.log('   SUPABASE_ANON_KEY:', !!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)

    // Check session status
    const { data: { session }, error } = await supabase.auth.getSession()
    console.log('3. Current session:', {
      exists: !!session,
      user: session?.user?.email,
      expiresAt: session?.expires_at,
      error: error?.message
    })

    // Check localStorage for session data
    console.log('4. Local storage check:')
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i)
      if (key?.includes('supabase')) {
        console.log(`   ${key}: ${localStorage.getItem(key)?.length} chars`)
      }
    }

    // Check cookies
    console.log('5. Cookies:', document.cookie.includes('sb-') ? 'Present' : 'Missing')

    // Check network connectivity to Supabase
    try {
      const response = await fetch(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/`, {
        method: 'HEAD',
        headers: {
          'apikey': process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ''
        }
      })
      console.log('6. Supabase connectivity:', response.ok ? 'OK' : 'Failed')
    } catch (err) {
      console.log('6. Supabase connectivity: Error', err)
    }

    console.groupEnd()
  }

  static async fixSessionPersistence() {
    try {
      // Clear potentially corrupted session data
      localStorage.removeItem('sb-' + process.env.NEXT_PUBLIC_SUPABASE_URL?.split('//')[1] + '-auth-token')
      
      // Reinitialize auth
      await supabase.auth.getSession()
      
      console.log('Session persistence fix applied')
    } catch (error) {
      console.error('Failed to fix session persistence:', error)
    }
  }
}

// Hook for session debugging
export function useSessionDebug() {
  useEffect(() => {
    if (process.env.NODE_ENV === 'development') {
      // Auto-diagnose on mount
      SessionDebugger.diagnoseSessionIssues()

      // Listen for auth state changes
      const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
        console.log('Auth state change:', event, session?.user?.email)
      })

      return () => subscription.unsubscribe()
    }
  }, [])
}

Handling Auth Callback Errors

Improve error handling for authentication callbacks:

// app/auth/callback/page.tsx - Enhanced error handling
'use client'
import { useEffect, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { supabase } from '@/lib/supabase'

interface AuthError {
  type: 'validation' | 'network' | 'configuration' | 'unknown'
  message: string
  details?: any
}

export default function AuthCallback() {
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<AuthError | null>(null)
  const router = useRouter()
  const searchParams = useSearchParams()

  useEffect(() => {
    handleAuthCallback()
  }, [])

  const handleAuthCallback = async () => {
    try {
      const code = searchParams.get('code')
      const errorParam = searchParams.get('error')
      const errorDescription = searchParams.get('error_description')
      const accessToken = searchParams.get('access_token')
      const refreshToken = searchParams.get('refresh_token')
      const tokenType = searchParams.get('token_type')

      // Handle different error scenarios
      if (errorParam) {
        setError({
          type: 'validation',
          message: getErrorMessage(errorParam, errorDescription),
          details: { error: errorParam, description: errorDescription }
        })
        setLoading(false)
        return
      }

      // Handle PKCE flow (modern OAuth)
      if (code) {
        const { data, error } = await supabase.auth.exchangeCodeForSession(code)
        
        if (error) {
          setError({
            type: error.message.includes('network') ? 'network' : 'validation',
            message: error.message,
            details: error
          })
        } else if (data.session) {
          // Successful authentication
          const redirectTo = sessionStorage.getItem('auth-redirect-to') || '/dashboard'
          sessionStorage.removeItem('auth-redirect-to')
          router.replace(redirectTo)
          return
        }
      }

      // Handle legacy token-based flow
      else if (accessToken && refreshToken) {
        const { data, error } = await supabase.auth.setSession({
          access_token: accessToken,
          refresh_token: refreshToken
        })

        if (error) {
          setError({
            type: 'validation',
            message: error.message,
            details: error
          })
        } else if (data.session) {
          router.replace('/dashboard')
          return
        }
      }

      // No valid auth parameters found
      else {
        setError({
          type: 'configuration',
          message: 'No valid authentication parameters found in the callback URL.',
          details: { searchParams: Object.fromEntries(searchParams.entries()) }
        })
      }

    } catch (err: any) {
      setError({
        type: 'unknown',
        message: 'An unexpected error occurred during authentication.',
        details: err
      })
    }

    setLoading(false)
  }

  const getErrorMessage = (error: string, description?: string | null): string => {
    const errorMessages: Record<string, string> = {
      'access_denied': 'You denied access to the application. Please try again if this was unintentional.',
      'invalid_request': 'The authentication request was invalid. Please try signing in again.',
      'unauthorized_client': 'The application is not authorized for this authentication method.',
      'unsupported_response_type': 'The authentication provider doesn\'t support this flow.',
      'invalid_scope': 'The requested permissions are invalid.',
      'server_error': 'The authentication provider encountered an error. Please try again.',
      'temporarily_unavailable': 'The authentication service is temporarily unavailable. Please try again later.'
    }

    return errorMessages[error] || description || 'An authentication error occurred.'
  }

  const handleRetry = () => {
    setError(null)
    setLoading(true)
    handleAuthCallback()
  }

  if (loading) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="text-center space-y-4">
          <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
          <p className="text-gray-600">Completing authentication...</p>
          <p className="text-sm text-gray-500">This may take a few seconds</p>
        </div>
      </div>
    )
  }

  if (error) {
    return (
      <div className="min-h-screen flex items-center justify-center p-4">
        <div className="max-w-md w-full space-y-6">
          <div className="text-center">
            <div className="w-12 h-12 mx-auto bg-red-100 rounded-full flex items-center justify-center">
              <svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
              </svg>
            </div>
            <h2 className="mt-4 text-lg font-semibold text-gray-900">Authentication Failed</h2>
            <p className="mt-2 text-gray-600">{error.message}</p>
          </div>

          <div className="space-y-3">
            <button
              onClick={handleRetry}
              className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors"
            >
              Try Again
            </button>
            <button
              onClick={() => router.push('/auth/login')}
              className="w-full bg-gray-200 text-gray-900 py-2 px-4 rounded-md hover:bg-gray-300 transition-colors"
            >
              Back to Sign In
            </button>
          </div>

          {process.env.NODE_ENV === 'development' && (
            <details className="mt-4">
              <summary className="text-sm text-gray-500 cursor-pointer">Debug Information</summary>
              <pre className="mt-2 text-xs bg-gray-100 p-2 rounded overflow-auto">
                {JSON.stringify(error.details, null, 2)}
              </pre>
            </details>
          )}
        </div>
      </div>
    )
  }

  return null
}

Debugging Server vs. Client Side Auth Conflicts

Resolve SSR/CSR authentication conflicts:

// components/AuthSyncProvider.tsx
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import { User } from '@supabase/supabase-js'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'

interface AuthSyncContextType {
  user: User | null
  loading: boolean
  isHydrated: boolean
}

const AuthSyncContext = createContext<AuthSyncContextType>({
  user: null,
  loading: true,
  isHydrated: false
})

export function AuthSyncProvider({ 
  children,
  serverUser 
}: { 
  children: React.ReactNode
  serverUser?: User | null 
}) {
  const [user, setUser] = useState<User | null>(serverUser || null)
  const [loading, setLoading] = useState(true)
  const [isHydrated, setIsHydrated] = useState(false)
  const supabase = createClientComponentClient()

  useEffect(() => {
    // Mark as hydrated after first render
    setIsHydrated(true)

    const getSession = async () => {
      const { data: { session } } = await supabase.auth.getSession()
      
      // Check for conflicts between server and client state
      if (serverUser && session?.user) {
        if (serverUser.id !== session.user.id) {
          console.warn('Server/client user mismatch detected, using client state')
        }
      }

      setUser(session?.user ?? null)
      setLoading(false)
    }

    getSession()

    // Listen for auth changes
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        setUser(session?.user ?? null)
        setLoading(false)

        // Handle specific events
        switch (event) {
          case 'SIGNED_IN':
            console.log('User signed in:', session?.user?.email)
            break
          case 'SIGNED_OUT':
            console.log('User signed out')
            setUser(null)
            break
          case 'TOKEN_REFRESHED':
            console.log('Token refreshed for:', session?.user?.email)
            break
        }
      }
    )

    return () => subscription.unsubscribe()
  }, [supabase, serverUser])

  return (
    <AuthSyncContext.Provider value={{ user, loading, isHydrated }}>
      {children}
    </AuthSyncContext.Provider>
  )
}

export const useAuthSync = () => {
  const context = useContext(AuthSyncContext)
  if (!context) {
    throw new Error('useAuthSync must be used within AuthSyncProvider')
  }
  return context
}

// Higher-order component for handling SSR auth
export function withAuthSync<P extends object>(
  Component: React.ComponentType<P>,
  options: { requireAuth?: boolean } = {}
) {
  return function AuthSyncedComponent(props: P) {
    const { user, loading, isHydrated } = useAuthSync()
    const router = useRouter()

    // Wait for hydration to avoid SSR/CSR mismatches
    if (!isHydrated) {
      return <div>Loading...</div>
    }

    // Handle auth requirements
    if (options.requireAuth && !loading && !user) {
      router.push('/auth/login')
      return null
    }

    return <Component {...props} />
  }
}

// Usage in layout or pages
export default function RootLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <AuthSyncProvider>
          {children}
        </AuthSyncProvider>
      </body>
    </html>
  )
}

Conclusion

What You’ve Built and Secured

Throughout this comprehensive guide, you’ve implemented a production-ready authentication system that provides:

Complete Authentication Infrastructure:

  • Multi-method authentication (email/password, OAuth, magic links)
  • Secure session management with automatic refresh
  • Comprehensive user profile management
  • Password reset and email verification flows

Enterprise-Grade Security:

  • Row Level Security policies for data protection
  • JWT token verification and validation
  • Rate limiting and abuse prevention
  • Secure logout and session revocation

Developer-Friendly Features:

  • TypeScript integration for type safety
  • Comprehensive error handling and debugging tools
  • Real-time authentication state management
  • Production deployment configuration

User Experience Excellence:

  • Seamless authentication flows across devices
  • Responsive design with accessibility considerations
  • Progressive enhancement with graceful fallbacks
  • Clear user feedback and error messaging

Where to Go From Here: Advanced Features and Auth Strategies

Expand your authentication system with these advanced capabilities:

Enhanced Security Features:

  • Two-factor authentication with TOTP or SMS
  • Biometric authentication for mobile applications
  • Device fingerprinting and suspicious activity detection
  • Advanced audit logging and compliance reporting

User Experience Improvements:

  • Single Sign-On (SSO) integration for enterprise users
  • Social account linking and identity consolidation
  • Progressive user onboarding with conditional requirements
  • Personalized authentication experiences based on user behavior

Administrative Capabilities:

  • User management dashboard for administrators
  • Role-based access control with granular permissions
  • Bulk user operations and data export capabilities
  • Advanced analytics and user behavior insights

Integration Opportunities:

  • Customer relationship management (CRM) system integration
  • Marketing automation platform connections
  • Analytics and business intelligence tool integration
  • Third-party identity provider federations

Resources to Explore Supabase and Next.js Further

Continue your development journey with these essential resources:

Official Documentation and Guides:

Community Resources and Examples:

Security and Best Practices:

The authentication system you’ve built provides a solid foundation for any modern web application. As you continue developing, remember that security is an ongoing process requiring regular updates, monitoring, and adaptation to new threats and requirements. The combination of Supabase and Next.js offers the flexibility to grow and evolve your authentication system as your application scales and your security needs become more sophisticated.

Focus on understanding the underlying security principles rather than just following implementation patterns. This knowledge will serve you well as authentication technologies continue to evolve and new security challenges emerge in the ever-changing landscape of web development.

Comments (0)

Comment


Note: All Input Fields are required.