Supabase Authentication & Authorization Patterns
Developer Guide

Supabase Authentication & Authorization Patterns

Master Supabase authentication and authorization. Learn email/password auth, social logins, magic links, 2FA, row-level security policies, and role-based...

2026-02-16
42 min read
Supabase Authentication & Authorization Patterns

Supabase Authentication & Authorization Patterns#

Authentication and authorization are the foundation of secure applications. Supabase provides a complete auth system with email/password, social logins, magic links, and powerful row-level security. This guide covers everything you need to build secure, production-ready authentication.

Why Supabase Auth?#

Built-in Features:

  • Email/password authentication
  • Social OAuth providers (Google, GitHub, etc.)
  • Magic link (passwordless) authentication
  • Multi-factor authentication (2FA)
  • Session management
  • JWT tokens

Security:

  • Row-level security (RLS) policies
  • Automatic password hashing
  • Secure session storage
  • PKCE flow for OAuth

Developer Experience:

  • Simple API
  • Auto-generated types
  • Real-time auth state changes
  • Works seamlessly with Next.js

1. Supabase Auth Fundamentals#

Client Setup#

// 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!
  )
}

Server Setup#

// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export 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
        },
      },
    }
  )
}

Auth State Management#

'use client'

import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'
import type { User } from '@supabase/supabase-js'

export function useUser() {
  const [user, setUser] = useState<User | null>(null)
  const supabase = createClient()

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

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

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

  return user
}

2. Email/Password Authentication#

Sign Up#

'use client'

import { createClient } from '@/lib/supabase/client'
import { useState } from 'react'

export function SignUpForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [loading, setLoading] = useState(false)
  const supabase = createClient()

  async function handleSignUp(e: React.FormEvent) {
    e.preventDefault()
    setLoading(true)

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

    if (error) {
      alert(error.message)
    } else {
      alert('Check your email for the confirmation link!')
    }

    setLoading(false)
  }

  return (
    <form onSubmit={handleSignUp}>
      <input
        type="email"
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required
      />
      <input
        type="password"
        placeholder="Password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        required
        minLength={6}
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Signing up...' : 'Sign Up'}
      </button>
    </form>
  )
}

Sign In#

'use client'

import { createClient } from '@/lib/supabase/client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'

export function SignInForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  const supabase = createClient()

  async function handleSignIn(e: React.FormEvent) {
    e.preventDefault()
    setLoading(true)

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

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

  return (
    <form onSubmit={handleSignIn}>
      <input
        type="email"
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required
      />
      <input
        type="password"
        placeholder="Password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Signing in...' : 'Sign In'}
      </button>
    </form>
  )
}

Password Reset#

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

// Update password (on reset page)
const { error } = await supabase.auth.updateUser({
  password: newPassword,
})

Related: Fix Supabase Auth Session Not Persisting After Refresh Next.js 14, Supabase Auth Redirect Not Working Next.js App Router Solution

3. Social Authentication#

Configure OAuth Providers#

In Supabase Dashboard:

  1. Go to Authentication → Providers
  2. Enable desired providers (Google, GitHub, etc.)
  3. Add OAuth credentials from provider

Google OAuth#

'use client'

import { createClient } from '@/lib/supabase/client'

export function GoogleSignIn() {
  const supabase = createClient()

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

    if (error) {
      alert(error.message)
    }
  }

  return (
    <button onClick={handleGoogleSignIn}>
      Sign in with Google
    </button>
  )
}

GitHub OAuth#

const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: `${window.location.origin}/auth/callback`,
  },
})

OAuth Callback Handler#

// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
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 = createClient()
    await supabase.auth.exchangeCodeForSession(code)
  }

  return NextResponse.redirect(new URL('/dashboard', request.url))
}

Related: Implement Social Auth with Supabase Next.js Complete Guide, Supabase OAuth Providers Configuration

'use client'

import { createClient } from '@/lib/supabase/client'
import { useState } from 'react'

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

  async function handleMagicLink(e: React.FormEvent) {
    e.preventDefault()
    setLoading(true)

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

    if (error) {
      alert(error.message)
    } else {
      setSent(true)
    }

    setLoading(false)
  }

  if (sent) {
    return <p>Check your email for the magic link!</p>
  }

  return (
    <form onSubmit={handleMagicLink}>
      <input
        type="email"
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Sending...' : 'Send Magic Link'}
      </button>
    </form>
  )
}

Related: Implement Supabase Magic Link Auth in Next.js 15, Supabase Email Confirmation Not Sending Troubleshooting Guide

5. Multi-Factor Authentication (2FA)#

Enable 2FA#

'use client'

import { createClient } from '@/lib/supabase/client'
import { useState } from 'react'

export function Enable2FA() {
  const [qrCode, setQrCode] = useState('')
  const [secret, setSecret] = useState('')
  const supabase = createClient()

  async function enrollMFA() {
    const { data, error } = await supabase.auth.mfa.enroll({
      factorType: 'totp',
    })

    if (error) {
      alert(error.message)
      return
    }

    setQrCode(data.totp.qr_code)
    setSecret(data.totp.secret)
  }

  async function verifyMFA(code: string) {
    const { data, error } = await supabase.auth.mfa.challengeAndVerify({
      factorId: data.id,
      code,
    })

    if (error) {
      alert(error.message)
    } else {
      alert('2FA enabled successfully!')
    }
  }

  return (
    <div>
      <button onClick={enrollMFA}>Enable 2FA</button>
      {qrCode && (
        <div>
          <img src={qrCode} alt="QR Code" />
          <p>Secret: {secret}</p>
          <input
            type="text"
            placeholder="Enter 6-digit code"
            onChange={(e) => {
              if (e.target.value.length === 6) {
                verifyMFA(e.target.value)
              }
            }}
          />
        </div>
      )}
    </div>
  )
}

Related: Add Two-Factor Authentication 2FA with Supabase, Build Custom Auth Flow with Supabase and Next.js

6. Row-Level Security (RLS) Policies#

Enable RLS#

-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

Basic RLS Policies#

-- Users can only read their own posts
CREATE POLICY "Users can view own posts"
  ON posts FOR SELECT
  USING (auth.uid() = user_id);

-- Users can insert their own posts
CREATE POLICY "Users can create posts"
  ON posts FOR INSERT
  WITH CHECK (auth.uid() = user_id);

-- Users can update their own posts
CREATE POLICY "Users can update own posts"
  ON posts FOR UPDATE
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

-- Users can delete their own posts
CREATE POLICY "Users can delete own posts"
  ON posts FOR DELETE
  USING (auth.uid() = user_id);

Public Read, Authenticated Write#

-- Anyone can read posts
CREATE POLICY "Public posts are viewable by everyone"
  ON posts FOR SELECT
  USING (published = true);

-- Only authenticated users can create posts
CREATE POLICY "Authenticated users can create posts"
  ON posts FOR INSERT
  WITH CHECK (auth.role() = 'authenticated');

Organization-Based Access#

-- Users can only see posts from their organization
CREATE POLICY "Users can view organization posts"
  ON posts FOR SELECT
  USING (
    organization_id IN (
      SELECT organization_id 
      FROM organization_members 
      WHERE user_id = auth.uid()
    )
  );

Related: Implement Supabase Row Level Security RLS Policies, Supabase RLS Policies Common Mistakes and Solutions

7. Custom Claims and Permissions#

Add Custom Claims#

-- Create function to get user role
CREATE OR REPLACE FUNCTION auth.user_role()
RETURNS TEXT AS $$
  SELECT role FROM public.users WHERE id = auth.uid()
$$ LANGUAGE SQL STABLE;

-- Use in RLS policy
CREATE POLICY "Admins can view all posts"
  ON posts FOR SELECT
  USING (auth.user_role() = 'admin');

Role-Based Access Control (RBAC)#

-- Create roles table
CREATE TABLE roles (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name TEXT UNIQUE NOT NULL,
  permissions JSONB NOT NULL DEFAULT '{}'::jsonb
);

-- Create user_roles table
CREATE TABLE user_roles (
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
  PRIMARY KEY (user_id, role_id)
);

-- Function to check permission
CREATE OR REPLACE FUNCTION has_permission(permission TEXT)
RETURNS BOOLEAN AS $$
  SELECT EXISTS (
    SELECT 1
    FROM user_roles ur
    JOIN roles r ON ur.role_id = r.id
    WHERE ur.user_id = auth.uid()
      AND r.permissions ? permission
  )
$$ LANGUAGE SQL STABLE;

-- Use in RLS policy
CREATE POLICY "Users with edit permission can update posts"
  ON posts FOR UPDATE
  USING (has_permission('posts.edit'));

Related: Implement Role-Based Access Control RBAC Supabase, Handle Supabase Auth Errors in Next.js Middleware

8. Session Management#

Get Current Session#

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

Refresh Session#

const { data: { session }, error } = await supabase.auth.refreshSession()

Sign Out#

const { error } = await supabase.auth.signOut()

Session Expiry#

// Check if session is expired
const { data: { session } } = await supabase.auth.getSession()

if (session) {
  const expiresAt = session.expires_at * 1000 // Convert to milliseconds
  const now = Date.now()
  
  if (expiresAt < now) {
    // Session expired, refresh it
    await supabase.auth.refreshSession()
  }
}

Related: Implement Session Refresh Logic Supabase Next.js, Handle Auth State Persistence in Next.js App Router

9. Protected Routes#

Middleware Protection#

// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  const response = NextResponse.next()
  
  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: any) {
          response.cookies.set({ name, value, ...options })
        },
        remove(name: string, options: any) {
          response.cookies.set({ name, value: '', ...options })
        },
      },
    }
  )

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

  // Redirect to login if not authenticated
  if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/auth/login', request.url))
  }

  // Redirect to dashboard if already authenticated
  if (session && request.nextUrl.pathname.startsWith('/auth')) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return response
}

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

Server Component Protection#

// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const supabase = createClient()
  
  const { data: { session } } = await supabase.auth.getSession()
  
  if (!session) {
    redirect('/auth/login')
  }

  return <div>Dashboard content</div>
}

Related: Build Protected Routes in Next.js 15 with Supabase, Handle Auth State Persistence in Next.js App Router

10. Common Auth Patterns and Anti-Patterns#

✅ Good Patterns#

Server-Side Session Validation

// Always validate on server
const supabase = createClient()
const { data: { session } } = await supabase.auth.getSession()

if (!session) {
  return { error: 'Unauthorized' }
}

Secure Cookie Storage

// Use httpOnly cookies for session storage
const supabase = createServerClient(url, key, {
  cookies: {
    get(name) { return cookies().get(name)?.value },
  },
})

RLS for Data Access

-- Always use RLS policies, never trust client
CREATE POLICY "Users can only access their data"
  ON table_name
  USING (user_id = auth.uid());

❌ Anti-Patterns#

Client-Side Only Auth

// ❌ Bad: Only checking auth on client
'use client'
const { data: { user } } = await supabase.auth.getUser()
if (!user) return <Login />

Bypassing RLS

// ❌ Bad: Using service role key on client
const supabase = createClient(url, serviceRoleKey) // Never do this!

Storing Sensitive Data in JWT

// ❌ Bad: JWT is visible to client
const { data: { session } } = await supabase.auth.getSession()
// session.access_token is readable by client

11. Testing Auth Flows#

Test RLS Policies#

-- Test as specific user
SET request.jwt.claim.sub = 'user-uuid-here';
SELECT * FROM posts; -- Should only return user's posts

-- Reset
RESET request.jwt.claim.sub;

Integration Tests#

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

describe('Auth', () => {
  it('should sign up user', async () => {
    const supabase = createClient(url, key)
    
    const { data, error } = await supabase.auth.signUp({
      email: 'test@example.com',
      password: 'password123',
    })
    
    expect(error).toBeNull()
    expect(data.user).toBeDefined()
  })
})

Related: Test Supabase RLS Policies Locally, Test Supabase Integration in Next.js with Jest

Frequently Asked Questions (FAQ)#

What authentication methods does Supabase support?#

Supabase supports email/password, magic links (passwordless), social OAuth (Google, GitHub, etc.), phone/SMS, and SAML SSO (Enterprise). You can enable multiple methods simultaneously and let users choose their preferred login method.

How do I implement Row Level Security (RLS)?#

Enable RLS with ALTER TABLE table_name ENABLE ROW LEVEL SECURITY, then create policies using CREATE POLICY. Policies use auth.uid() to identify the current user and control access at the database level. Always enable RLS on tables with user data.

What's the difference between anon key and service role key?#

The anon key is safe for client-side use and respects RLS policies. The service role key bypasses ALL security including RLS and should NEVER be exposed to clients. Use service role key only in server-side code for admin operations.

How do I handle session management in Next.js?#

Create separate server and client Supabase clients. Use the server client in Server Components and API routes, browser client in Client Components. Implement middleware to refresh sessions automatically. Sessions expire after 1 hour by default.

Can I customize the authentication UI?#

Yes, Supabase provides headless authentication—you build your own UI. Use the Supabase client methods (signUp, signInWithPassword, etc.) with your custom forms. No pre-built UI components are provided, giving you complete design control.

How do I implement multi-factor authentication (2FA)?#

Use Supabase's MFA API with TOTP (Time-based One-Time Password). Call supabase.auth.mfa.enroll() to generate a QR code, users scan it with an authenticator app, then verify with challengeAndVerify(). MFA is available on Pro plan and above.

What's the best way to handle password resets?#

Use supabase.auth.resetPasswordForEmail() to send a reset link. Configure the redirect URL in your Supabase dashboard. Create a password reset page that calls supabase.auth.updateUser({ password: newPassword }) after verifying the reset token.

How do I implement social authentication?#

Enable OAuth providers in Supabase Dashboard → Authentication → Providers. Add OAuth credentials from the provider (Google, GitHub, etc.). Use signInWithOAuth({ provider: 'google' }) in your code. Configure callback URLs properly.

Can I use Supabase Auth with existing users?#

Yes, you can migrate existing users. Create users with supabase.auth.admin.createUser() using the service role key. Set passwords or send password reset emails. Migrate user data to your Supabase database alongside auth records.

How do I protect API routes?#

Check authentication in API routes by calling supabase.auth.getUser() with the server client. Return 401 Unauthorized if no user. Use RLS policies as a second layer of defense—never rely solely on API route checks.

What's the difference between getUser() and getSession()?#

getUser() validates the JWT token with Supabase servers (more secure, slower). getSession() reads the local session without validation (faster, less secure). Use getUser() for sensitive operations, getSession() for UI state.

How do I implement role-based access control (RBAC)?#

Create a roles table with permissions, a user_roles junction table, and database functions to check permissions. Use these functions in RLS policies: CREATE POLICY ... USING (has_permission('resource.action')). Store roles in your database, not in JWT.

Can I customize email templates?#

Yes, customize email templates in Supabase Dashboard → Authentication → Email Templates. Edit HTML/text for confirmation, password reset, magic link, and invitation emails. Use variables like {{ .ConfirmationURL }} for dynamic content.

How do I handle authentication errors?#

Wrap auth calls in try-catch blocks, check the error object for specific error codes, and show user-friendly messages. Common errors: invalid credentials, email already exists, weak password. Always log errors for debugging.

Conclusion#

Supabase authentication provides everything you need for secure, production-ready auth. Start with email/password or magic links, add social OAuth as needed, and protect your data with RLS policies.

Remember: always validate sessions on the server, use RLS policies for data access, and never expose service role keys to clients.

Build secure applications with confidence.

Frequently Asked Questions

|

Have more questions? Contact us