| name | auth-supabase |
| description | Implements standard Supabase authentication flows including signup, login, password reset, OAuth providers, email verification, and session management with complete security best practices |
Supabase Authentication Implementation Standards
This skill provides comprehensive guidelines for implementing authentication using Supabase, covering all authentication patterns, security practices, and environment configuration.
🚨 CRITICAL REQUIREMENTS - MUST READ FIRST
Package Versions - MANDATORY (Security Critical)
✅ REQUIRED VERSIONS (Latest Stable):
{
"dependencies": {
"@supabase/supabase-js": "^2.89.0",
"@supabase/ssr": "^0.8.0",
"next": "^16.1.1"
}
}
❌ DEPRECATED PACKAGES - DO NOT USE:
{
"@supabase/auth-helpers-nextjs": "...",
"@supabase/auth-helpers-react": "...",
"@supabase/auth-ui-react": "..."
}
🔒 Security Rule:
- Always use
@supabase/ssr for Next.js 13+ App Router
- Never use deprecated
auth-helpers packages
- Update packages monthly:
npm update @supabase/supabase-js @supabase/ssr
Next.js 14 Server Actions - MANDATORY PATTERN
❌ ANTI-PATTERN - NEVER DO THIS:
'use client';
export default function LoginPage() {
async function login(formData: FormData) {
'use server';
}
}
✅ REQUIRED PATTERN - ALWAYS USE THIS:
'use server';
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export async function loginAction(formData: FormData) {
const supabase = await createClient()
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
}
const { error } = await supabase.auth.signInWithPassword(data)
if (error) {
return { error: error.message }
}
redirect('/dashboard')
}
export async function signUpAction(formData: FormData) {
const supabase = await createClient()
const data = {
: formData.() ,
: formData.() ,
: {
: {
: formData.() ,
},
},
}
{ error } = supabase..(data)
(error) {
{ : error. }
}
()
}
() {
supabase = ()
supabase..()
()
}
;
{ loginAction }
{ useState }
() {
[error, setError] = useState< | >()
(
)
}
📁 MANDATORY File Structure:
app/
├── actions/
│ └── auth.ts ✅ All auth server actions here
├── lib/
│ └── supabase/
│ ├── client.ts ✅ Browser client
│ ├── server.ts ✅ Server client
│ └── middleware.ts ✅ Middleware client
├── (auth)/
│ ├── login/
│ │ └── page.tsx ✅ Client component
│ ├── register/
│ │ └── page.tsx ✅ Client component
│ └── callback/
│ └── route.ts ✅ Route handler
└── middleware.ts ✅ Session refresh
🔒 RULES:
- All auth server actions →
app/actions/auth.ts with 'use server'
- All auth pages → Client components with
'use client'
- NO mixing of directives in same file
- Use
async/await for all Supabase calls
Initial Setup Checklist
Environment Variables
Required Variables (.env.local):
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_REDIRECT_URL=http://localhost:3000/auth/callback
SUPABASE_SMTP_HOST=smtp.sendgrid.net
SUPABASE_SMTP_PORT=587
SUPABASE_SMTP_USER=apikey
SUPABASE_SMTP_PASS=your-sendgrid-api-key
SUPABASE_SMTP_SENDER_EMAIL=noreply@yourdomain.com
SUPABASE_SMTP_SENDER_NAME=Your App Name
Production Variables:
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
NEXT_PUBLIC_REDIRECT_URL=https://yourdomain.com/auth/callback
Supabase Dashboard Configuration
-
Authentication Settings (Authentication > Settings)
-
Email Templates (Authentication > Email Templates)
-
OAuth Providers (if using social auth)
Supabase Client Initialization
Next.js App Router Implementation
Create Supabase client utilities:
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options })
} catch (error) {
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.({ name, : , ...options })
} (error) {
}
},
},
}
)
}
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({
name,
value,
...options,
})
response = .({
: {
: request.,
},
})
response..({
name,
value,
...options,
})
},
() {
request..({
name,
: ,
...options,
})
response = .({
: {
: request.,
},
})
response..({
name,
: ,
...options,
})
},
},
}
)
supabase..()
response
}
Middleware configuration:
import { updateSession } from '@/lib/supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
Authentication Flows
1. Sign Up Flow
✅ REQUIRED IMPLEMENTATION:
'use server';
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export async function signUpAction(formData: FormData) {
const supabase = await createClient()
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
options: {
data: {
full_name: formData.get('full_name') as string,
},
emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
},
}
const { error } = await supabase.auth.signUp(data)
if (error) {
return { error: error.message }
}
redirect('/auth/confirm')
}
Sign Up Component (REQUIRED PATTERN):
'use client';
import { signUpAction } from '@/app/actions/auth'
import { useState } from 'react'
export default function SignUpPage() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="w-full max-w-md">
<h1 className="text-2xl font-bold text-start mb-6">
Create Account
</h1>
{error && (
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg mb-4 text-start">
{error}
</div>
)}
<form
action={async (formData) => {
setLoading(true)
const result = await signUpAction(formData)
setLoading(false)
if (result?.error) {
setError(result.error)
}
}}
className="space-y-4"
>
<>
Full Name
Email
Password
{loading ? 'Creating account...' : 'Sign Up'}
)
}
2. Login Flow
✅ REQUIRED IMPLEMENTATION:
'use server';
export async function loginAction(formData: FormData) {
const supabase = await createClient()
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
}
const { error } = await supabase.auth.signInWithPassword(data)
if (error) {
return { error: error.message }
}
redirect('/dashboard')
}
Login Component (REQUIRED PATTERN):
'use client';
import { loginAction } from '@/app/actions/auth'
import { useState } from 'react'
import Link from 'next/link'
export default function LoginPage() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="w-full max-w-md">
<h1 className="text-2xl font-bold text-start mb-6">
Sign In
</h1>
{error && (
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg mb-4 text-start">
{error}
</div>
)}
<form
action={async (formData) => {
setLoading(true)
const result = await loginAction(formData)
setLoading(false)
if (result?.error) {
setError(result.error)
}
}}
className="space-y-4"
>
Email
Password
Forgot password?
{loading ? 'Signing in...' : 'Sign In'}
Don't have an account?{' '}
Sign up
)
}
3. Password Reset Flow
'use server';
export async function requestPasswordResetAction(formData: FormData) {
const supabase = await createClient()
const email = formData.get('email') as string
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
})
if (error) {
return { error: error.message }
}
return { success: true }
}
export async function resetPasswordAction(formData: FormData) {
const supabase = await createClient()
const password = formData.get('password') as string
const { error } = await supabase.auth.updateUser({
password: password,
})
(error) {
{ : error. }
}
()
}
4. OAuth Login (Google, GitHub, etc.)
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const supabase = await createClient()
const provider = request.nextUrl.searchParams.get('provider') as 'google' | 'github'
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
},
})
if (error) {
redirect('/login?error=Could not authenticate')
}
if (data.url) {
redirect(data.url)
}
}
OAuth Button Component:
<a href="/auth/oauth?provider=google">
Sign in with Google
</a>
<a href="/auth/oauth?provider=github">
Sign in with GitHub
</a>
5. Auth Callback Handler
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
if (code) {
const supabase = await createClient()
await supabase.auth.exchangeCodeForSession(code)
}
return NextResponse.redirect(`${requestUrl.origin}/dashboard`)
}
6. Logout Flow
'use server'
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export async function logout() {
const supabase = await createClient()
await supabase.auth.signOut()
redirect('/login')
}
Session Management
Check Authentication Status
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function ProtectedPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
return <div>Welcome, {user.email}!</div>
}
'use client'
import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'
import type { User } from '@supabase/supabase-js'
export default function ClientComponent() {
const [user, setUser] = useState<User | null>(null)
const supabase = createClient()
useEffect(() => {
const getUser = async () => {
const { data: { user } } = await supabase.auth.getUser()
setUser(user)
}
getUser()
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
setUser(session?.user ?? null)
}
)
return () => subscription.unsubscribe()
}, [supabase])
return
}
Security Best Practices
1. Row Level Security (RLS)
Enable RLS on all tables:
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
2. Password Requirements
const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[0-9]/, 'Password must contain at least one number')
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character')
3. Rate Limiting
Configure in Supabase Dashboard:
Authentication > Settings > Rate Limits
- Recommended: 10 requests per 10 seconds per IP
4. Email Verification
const { data: { user } } = await supabase.auth.getUser()
if (user && !user.email_confirmed_at) {
redirect('/auth/verify-email')
}
5. CSRF Protection
Error Handling
export async function handleAuthError(error: any) {
const errorMessages: Record<string, string> = {
'Invalid login credentials': 'Email or password is incorrect',
'Email not confirmed': 'Please verify your email address',
'User already registered': 'An account with this email already exists',
'Password should be at least 8 characters': 'Password must be at least 8 characters long',
}
return errorMessages[error.message] || 'An unexpected error occurred. Please try again.'
}
Testing Checklist
🚨 CRITICAL CHECKS - Must Pass All:
✅ Functional Tests:
🔍 Code Quality Checks:
grep -r "@supabase/auth-helpers" package.json
grep -r "@supabase/ssr" package.json
grep -l "'use client'" app/actions/*.ts
grep -l "'use server'" app/\(auth\)/**/*.tsx
grep -r "px-" app/\(auth\)
grep -r "pl-" app/\(auth\)
grep -r "pr-" app/\(auth\)
grep -r "text-left" app/\(auth\)
Common Pitfalls to Avoid
- Exposing service role key - Never use on client side
- Not setting redirect URLs - Causes OAuth failures
- Forgetting email confirmation - Users can't log in
- Not handling errors - Poor user experience
- Missing RLS policies - Security vulnerability
- Hardcoding URLs - Breaks in different environments
- Not refreshing sessions - Users logged out unexpectedly
- Weak password requirements - Security risk
- No rate limiting - Vulnerable to brute force
- Not testing OAuth flows - Production failures
Email Template Customization
See templates/email-templates.md for complete email template examples and customization guidelines.
Additional Resources
Security Note: Always audit your RLS policies, keep Supabase packages updated, and never expose service role keys to client-side code.