Supabase Auth
Library
Supabase Auth
Overview
Supabase Auth is the user authentication feature in the open-source Firebase alternative. Provides JWT, multi-factor authentication, OTP support, and Row Level Security (RLS) integration. Fully self-hostable avoiding vendor lock-in. One of the most promising authentication solutions for projects in 2025. Strongly recommended choice for new projects alongside NextAuth.js. Rapidly gaining support in open-source oriented developer community and optimized for database-driven application authentication.
Details
Supabase Auth 2025 is an authentication system deeply integrated with PostgreSQL database. JWT-based authentication provides consistent authentication experience on both client-side and server-side. Integration with Row Level Security (RLS) policies enables controlling authenticated user data access at row level. Multi-factor authentication (MFA) supports TOTP (Time-Based One Time Password) and manages security levels through Authenticator Assurance Level (AAL).
Key Features
- Database Integrated Authentication: Authentication system fully integrated with PostgreSQL RLS
- Complete JWT Support: Secure session management with access tokens and refresh tokens
- Multi-Factor Authentication: Comprehensive MFA support with TOTP, SMS, Email OTP
- Row Level Security: Fine-grained access control at database level
- Social Login: Support for major providers like Google, GitHub, Discord
- Self-Hostable: Completely open source enabling operation on own infrastructure
Pros and Cons
Pros
- Complete open source eliminates vendor lock-in and enables long-term cost reduction
- Deep PostgreSQL integration enables powerful database-driven authentication
- RLS provides authorization control at database level rather than application level
- Choice between self-hosting and managed service offers operational flexibility
- Full TypeScript support provides type-safe development environment
- Geographic routing support optimal for global-scale applications
Cons
- PostgreSQL requirement limits database choice constraints
- Operational and maintenance responsibility falls to development team when self-hosting
- Limited enterprise features (audit logs, advanced RBAC)
- Documentation and examples somewhat fewer compared to Firebase
- Custom authentication flows require development effort for complex scenarios
- Third-party library ecosystem still developing
Reference Pages
Code Examples
Basic Setup
# Install Supabase JavaScript client
npm install @supabase/supabase-js
# Additional packages for Next.js
npm install @supabase/auth-helpers-nextjs @supabase/auth-helpers-react
# React Hook Form (optional)
npm install react-hook-form @hookform/resolvers zod
Supabase Client Initial Configuration
// 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
}
})
// TypeScript type definitions generation
export type Database = {
public: {
Tables: {
profiles: {
Row: {
id: string
email: string
full_name: string | null
avatar_url: string | null
created_at: string
}
Insert: {
id: string
email: string
full_name?: string | null
avatar_url?: string | null
}
Update: {
full_name?: string | null
avatar_url?: string | null
}
}
}
}
}
export const supabaseTyped = createClient<Database>(supabaseUrl, supabaseAnonKey)
Authentication Provider Setup (Next.js App Router)
// app/layout.tsx
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { AuthProvider } from '@/components/auth-provider'
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const supabase = createServerComponentClient({ cookies })
const {
data: { session },
} = await supabase.auth.getSession()
return (
<html lang="en">
<body>
<AuthProvider session={session}>
{children}
</AuthProvider>
</body>
</html>
)
}
// components/auth-provider.tsx
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import type { Session, User } from '@supabase/supabase-js'
type AuthContextType = {
session: Session | null
user: User | null
signOut: () => Promise<void>
}
const AuthContext = createContext<AuthContextType>({
session: null,
user: null,
signOut: async () => {},
})
export function AuthProvider({
children,
session: initialSession
}: {
children: React.ReactNode
session: Session | null
}) {
const [session, setSession] = useState<Session | null>(initialSession)
const supabase = createClientComponentClient()
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
setSession(session)
})
return () => subscription.unsubscribe()
}, [supabase])
const signOut = async () => {
await supabase.auth.signOut()
}
return (
<AuthContext.Provider value={{
session,
user: session?.user ?? null,
signOut
}}>
{children}
</AuthContext.Provider>
)
}
export const useAuth = () => useContext(AuthContext)
Sign-up and Sign-in Forms
// components/auth-form.tsx
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const authSchema = z.object({
email: z.string().email('Please enter a valid email address'),
password: z.string().min(6, 'Password must be at least 6 characters'),
})
type AuthFormData = z.infer<typeof authSchema>
export function AuthForm({ mode }: { mode: 'sign-in' | 'sign-up' }) {
const [isLoading, setIsLoading] = useState(false)
const [message, setMessage] = useState('')
const router = useRouter()
const supabase = createClientComponentClient()
const {
register,
handleSubmit,
formState: { errors },
} = useForm<AuthFormData>({
resolver: zodResolver(authSchema),
})
const onSubmit = async (data: AuthFormData) => {
setIsLoading(true)
setMessage('')
try {
if (mode === 'sign-up') {
const { error } = await supabase.auth.signUp({
email: data.email,
password: data.password,
options: {
emailRedirectTo: `${location.origin}/auth/callback`
}
})
if (error) throw error
setMessage('Confirmation email sent. Please check your email.')
} else {
const { error } = await supabase.auth.signInWithPassword({
email: data.email,
password: data.password,
})
if (error) throw error
router.push('/dashboard')
router.refresh()
}
} catch (error: any) {
setMessage(error.message)
} finally {
setIsLoading(false)
}
}
const handleSocialSignIn = async (provider: 'google' | 'github') => {
const { error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${location.origin}/auth/callback`
}
})
if (error) {
setMessage(error.message)
}
}
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-6 text-center">
{mode === 'sign-up' ? 'Create Account' : 'Sign In'}
</h2>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">
Email Address
</label>
<input
type="email"
{...register('email')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">
Password
</label>
<input
type="password"
{...register('password')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password.message}</p>
)}
</div>
<button
type="submit"
disabled={isLoading}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{isLoading ? 'Processing...' : mode === 'sign-up' ? 'Create Account' : 'Sign In'}
</button>
</form>
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">or</span>
</div>
</div>
<div className="mt-6 space-y-3">
<button
onClick={() => handleSocialSignIn('google')}
className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
Sign in with Google
</button>
<button
onClick={() => handleSocialSignIn('github')}
className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
Sign in with GitHub
</button>
</div>
</div>
{message && (
<div className={`mt-4 p-3 rounded-md ${
message.includes('Confirmation email')
? 'bg-green-50 text-green-700 border border-green-200'
: 'bg-red-50 text-red-700 border border-red-200'
}`}>
{message}
</div>
)}
</div>
)
}
Row Level Security (RLS) Policy Configuration
-- Create profiles table
CREATE TABLE profiles (
id UUID REFERENCES auth.users ON DELETE CASCADE,
email TEXT UNIQUE NOT NULL,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (id)
);
-- Enable RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Users can only view their own profile
CREATE POLICY "Users can view own profile" ON profiles
FOR SELECT USING (auth.uid() = id);
-- Users can only update their own profile
CREATE POLICY "Users can update own profile" ON profiles
FOR UPDATE USING (auth.uid() = id);
-- Users can only insert their own profile
CREATE POLICY "Users can insert own profile" ON profiles
FOR INSERT WITH CHECK (auth.uid() = id);
-- Trigger for automatic profile creation
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, email, full_name)
VALUES (new.id, new.email, new.raw_user_meta_data->>'full_name');
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();
Multi-Factor Authentication (MFA) Implementation
// components/mfa-setup.tsx
'use client'
import { useState, useEffect } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import QRCode from 'qrcode'
export function MFASetup() {
const [qrCode, setQrCode] = useState('')
const [secret, setSecret] = useState('')
const [verifyCode, setVerifyCode] = useState('')
const [isEnabled, setIsEnabled] = useState(false)
const [loading, setLoading] = useState(false)
const supabase = createClientComponentClient()
useEffect(() => {
checkMFAStatus()
}, [])
const checkMFAStatus = async () => {
const { data: { user } } = await supabase.auth.getUser()
if (user) {
const { data: factors } = await supabase.auth.mfa.listFactors()
setIsEnabled(factors?.totp?.length > 0)
}
}
const enrollMFA = async () => {
setLoading(true)
try {
const { data, error } = await supabase.auth.mfa.enroll({
factorType: 'totp',
friendlyName: 'My Auth App',
})
if (error) throw error
setSecret(data.totp.secret)
// Generate QR code
const qrCodeUrl = data.totp.qr_code
const qrDataUrl = await QRCode.toDataURL(qrCodeUrl)
setQrCode(qrDataUrl)
} catch (error) {
console.error('MFA enrollment error:', error)
} finally {
setLoading(false)
}
}
const verifyAndEnable = async () => {
if (!verifyCode) return
setLoading(true)
try {
const { data, error } = await supabase.auth.mfa.challengeAndVerify({
factorId: secret, // This would be the actual factor ID from enrollment
code: verifyCode,
})
if (error) throw error
setIsEnabled(true)
alert('MFA successfully enabled!')
} catch (error) {
console.error('MFA verification error:', error)
alert('Invalid code. Please try again.')
} finally {
setLoading(false)
}
}
const unenrollMFA = async () => {
setLoading(true)
try {
const { data: factors } = await supabase.auth.mfa.listFactors()
if (factors?.totp?.length > 0) {
const { error } = await supabase.auth.mfa.unenroll({
factorId: factors.totp[0].id,
})
if (error) throw error
setIsEnabled(false)
setQrCode('')
setSecret('')
alert('MFA has been disabled.')
}
} catch (error) {
console.error('MFA unenroll error:', error)
} finally {
setLoading(false)
}
}
if (isEnabled) {
return (
<div className="p-6 bg-green-50 border border-green-200 rounded-lg">
<h3 className="text-lg font-semibold text-green-800 mb-2">
MFA is enabled ✓
</h3>
<p className="text-green-700 mb-4">
Your account is protected with multi-factor authentication.
</p>
<button
onClick={unenrollMFA}
disabled={loading}
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 disabled:opacity-50"
>
{loading ? 'Processing...' : 'Disable MFA'}
</button>
</div>
)
}
return (
<div className="p-6 border border-gray-200 rounded-lg">
<h3 className="text-lg font-semibold mb-4">Multi-Factor Authentication Setup</h3>
{!qrCode ? (
<div>
<p className="text-gray-600 mb-4">
We recommend enabling multi-factor authentication to improve security.
</p>
<button
onClick={enrollMFA}
disabled={loading}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Setting up...' : 'Setup MFA'}
</button>
</div>
) : (
<div>
<p className="text-sm text-gray-600 mb-4">
Scan the QR code with your authenticator app and enter the 6-digit code displayed.
</p>
<div className="mb-4 text-center">
<img src={qrCode} alt="MFA QR Code" className="mx-auto" />
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">
Authentication Code (6 digits)
</label>
<input
type="text"
value={verifyCode}
onChange={(e) => setVerifyCode(e.target.value)}
placeholder="123456"
maxLength={6}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
onClick={verifyAndEnable}
disabled={loading || verifyCode.length !== 6}
className="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 disabled:opacity-50"
>
{loading ? 'Verifying...' : 'Enable MFA'}
</button>
</div>
)}
</div>
)
}
Profile Management and Data Retrieval
// app/profile/page.tsx
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { ProfileForm } from '@/components/profile-form'
export default async function ProfilePage() {
const supabase = createServerComponentClient({ cookies })
const {
data: { session },
} = await supabase.auth.getSession()
if (!session) {
redirect('/auth/sign-in')
}
const { data: profile } = await supabase
.from('profiles')
.select('*')
.eq('id', session.user.id)
.single()
return (
<div className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Profile Settings</h1>
<ProfileForm profile={profile} />
</div>
)
}
// components/profile-form.tsx
'use client'
import { useState } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { useRouter } from 'next/navigation'
interface Profile {
id: string
email: string
full_name: string | null
avatar_url: string | null
}
export function ProfileForm({ profile }: { profile: Profile }) {
const [fullName, setFullName] = useState(profile.full_name || '')
const [avatarUrl, setAvatarUrl] = useState(profile.avatar_url || '')
const [loading, setLoading] = useState(false)
const supabase = createClientComponentClient()
const router = useRouter()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
const { error } = await supabase
.from('profiles')
.update({
full_name: fullName,
avatar_url: avatarUrl,
updated_at: new Date().toISOString(),
})
.eq('id', profile.id)
if (error) throw error
router.refresh()
alert('Profile updated successfully!')
} catch (error) {
console.error('Profile update error:', error)
alert('Failed to update profile.')
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium mb-2">
Email Address
</label>
<input
type="email"
value={profile.email}
disabled
className="w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-md"
/>
<p className="text-sm text-gray-500 mt-1">
Email address cannot be changed
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Full Name
</label>
<input
type="text"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Avatar URL
</label>
<input
type="url"
value={avatarUrl}
onChange={(e) => setAvatarUrl(e.target.value)}
placeholder="https://example.com/avatar.jpg"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</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 Profile'}
</button>
</form>
)
}