Claude-skill-registry auth-supabase
Implements standard Supabase authentication flows including signup, login, password reset, OAuth providers, email verification, and session management with complete security best practices
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/auth-supabase" ~/.claude/skills/majiayu000-claude-skill-registry-auth-supabase && rm -rf "$T"
skills/data/auth-supabase/SKILL.mdSupabase 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": "...", // ❌ DEPRECATED "@supabase/auth-helpers-react": "...", // ❌ DEPRECATED "@supabase/auth-ui-react": "..." // ❌ Optional, not required }
🔒 Security Rule:
- Always use
for Next.js 13+ App Router@supabase/ssr - Never use deprecated
packagesauth-helpers - Update packages monthly:
npm update @supabase/supabase-js @supabase/ssr
Next.js 14 Server Actions - MANDATORY PATTERN
❌ ANTI-PATTERN - NEVER DO THIS:
// ❌ WRONG - Server action inside client component 'use client'; export default function LoginPage() { async function login(formData: FormData) { 'use server'; // ❌ Mixed directives cause issues // ... } }
✅ REQUIRED PATTERN - ALWAYS USE THIS:
// app/actions/auth.ts 'use server'; // ✅ Dedicated file for all auth actions 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 = { email: formData.get('email') as string, password: formData.get('password') as string, options: { data: { full_name: formData.get('full_name') as string, }, }, } const { error } = await supabase.auth.signUp(data) if (error) { return { error: error.message } } redirect('/auth/confirm') } export async function logoutAction() { const supabase = await createClient() await supabase.auth.signOut() redirect('/login') } // app/(auth)/login/page.tsx 'use client'; // ✅ Separate client component file import { loginAction } from '@/app/actions/auth' import { useState } from 'react' export default function LoginPage() { const [error, setError] = useState<string | null>(null) return ( <form action={async (formData) => { const result = await loginAction(formData) if (result?.error) { setError(result.error) } }}> {/* Form fields */} </form> ) }
📁 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 →
withapp/actions/auth.ts'use server' - All auth pages → Client components with
'use client' - NO mixing of directives in same file
- Use
for all Supabase callsasync/await
Initial Setup Checklist
Environment Variables
Required Variables (.env.local):
# Supabase Core NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key # Supabase Service Role (Server-side only - NEVER expose to client) SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # Application URLs NEXT_PUBLIC_SITE_URL=http://localhost:3000 NEXT_PUBLIC_REDIRECT_URL=http://localhost:3000/auth/callback # Email Configuration (Optional - for custom SMTP) 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- Set Site URL:
https://yourdomain.com - Add redirect URLs:
(development)http://localhost:3000/auth/callback
(production)https://yourdomain.com/auth/callback
- Enable email confirmations (recommended)
- Configure session timeout (default: 1 week)
- Set Site URL:
-
Email Templates (
)Authentication > Email Templates- Customize confirmation email
- Customize password reset email
- Customize magic link email (if using)
- See
for examplestemplates/email-templates.md
-
OAuth Providers (if using social auth)
- Google: Add Client ID and Secret
- GitHub: Add Client ID and Secret
- Others as needed
Supabase Client Initialization
Next.js App Router Implementation
Create Supabase client utilities:
// 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! ) }
// lib/supabase/server.ts 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) { // Handle cookie setting in Server Component } }, remove(name: string, options: CookieOptions) { try { cookieStore.set({ name, value: '', ...options }) } catch (error) { // Handle cookie removal in Server Component } }, }, } ) }
// lib/supabase/middleware.ts 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 = NextResponse.next({ request: { headers: request.headers, }, }) response.cookies.set({ name, value, ...options, }) }, remove(name: string, options: CookieOptions) { request.cookies.set({ name, value: '', ...options, }) response = NextResponse.next({ request: { headers: request.headers, }, }) response.cookies.set({ name, value: '', ...options, }) }, }, } ) await supabase.auth.getUser() return response }
Middleware configuration:
// middleware.ts 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:
// app/actions/auth.ts '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):
// app/(auth)/register/page.tsx '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" > <div> <label htmlFor="full_name" className="block text-start mb-2 font-medium" > Full Name </label> <input id="full_name" name="full_name" type="text" dir="auto" className="w-full ps-4 pe-4 py-3 text-start rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200" placeholder="Enter your full name" required /> </div> <div> <label htmlFor="email" className="block text-start mb-2 font-medium" > Email </label> <input id="email" name="email" type="email" dir="auto" className="w-full ps-4 pe-4 py-3 text-start rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200" placeholder="Enter your email" required /> </div> <div> <label htmlFor="password" className="block text-start mb-2 font-medium" > Password </label> <input id="password" name="password" type="password" dir="auto" className="w-full ps-4 pe-4 py-3 text-start rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200" placeholder="Create a password" required minLength={8} /> </div> <button type="submit" disabled={loading} className="w-full py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" > {loading ? 'Creating account...' : 'Sign Up'} </button> </form> </div> </div> ) }
2. Login Flow
✅ REQUIRED IMPLEMENTATION:
// app/actions/auth.ts (add to existing file) '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):
// app/(auth)/login/page.tsx '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" > <div> <label htmlFor="email" className="block text-start mb-2 font-medium" > Email </label> <input id="email" name="email" type="email" dir="auto" className="w-full ps-4 pe-4 py-3 text-start rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200" placeholder="Enter your email" required /> </div> <div> <label htmlFor="password" className="block text-start mb-2 font-medium" > Password </label> <input id="password" name="password" type="password" dir="auto" className="w-full ps-4 pe-4 py-3 text-start rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200" placeholder="Enter your password" required minLength={8} /> </div> <div className="flex items-center justify-between"> <Link href="/auth/forgot-password" className="text-sm text-blue-600 hover:underline" > Forgot password? </Link> </div> <button type="submit" disabled={loading} className="w-full py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" > {loading ? 'Signing in...' : 'Sign In'} </button> <p className="text-center text-sm text-gray-600"> Don't have an account?{' '} <Link href="/register" className="text-blue-600 hover:underline"> Sign up </Link> </p> </form> </div> </div> ) }
3. Password Reset Flow
// app/actions/auth.ts (add to existing file) '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, }) if (error) { return { error: error.message } } redirect('/login?message=Password updated successfully') }
4. OAuth Login (Google, GitHub, etc.)
// app/auth/oauth/route.ts 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
// app/auth/callback/route.ts 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) } // Redirect to dashboard or origin URL return NextResponse.redirect(`${requestUrl.origin}/dashboard`) }
6. Logout Flow
// app/auth/logout/actions.ts '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
// Server Component 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> }
// Client Component '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 <div>{user ? `Logged in as ${user.email}` : 'Not logged in'}</div> }
Security Best Practices
1. Row Level Security (RLS)
Enable RLS on all tables:
-- Enable RLS ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; -- Users can only see 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);
2. Password Requirements
// Enforce strong passwords 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
// Require email verification before access const { data: { user } } = await supabase.auth.getUser() if (user && !user.email_confirmed_at) { redirect('/auth/verify-email') }
5. CSRF Protection
// Use Supabase's built-in CSRF protection via cookies // Already handled by @supabase/ssr package
Error Handling
// Comprehensive 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:
- Using
package (NOT deprecated@supabase/ssr
)auth-helpers - Package versions:
>= 2.39.0@supabase/supabase-js - All server actions in
withapp/actions/auth.ts'use server' - Auth pages use
(login, register)'use client' - NO files with both
AND'use client''use server' - All form labels have
classesblock text-start - All form inputs have
attributedir="auto" - All form inputs use
ps-*
(NOTpe-*
,px-*
,pl-*
)pr-* - Error messages displayed with proper RTL alignment
- Loading states implemented for all forms
✅ Functional Tests:
- Sign up with valid credentials works
- Sign up with duplicate email shows appropriate error
- Sign up with weak password shows validation error
- Email confirmation link works
- Login with verified account works
- Login with unverified account blocked (if required)
- Login with wrong password shows error
- Password reset email sent successfully
- Password reset link works and expires appropriately
- OAuth providers redirect correctly
- OAuth callback handles success/error states
- Logout clears session properly
- Protected routes redirect unauthenticated users
- Session persists across page refreshes
- Session expires after configured timeout
- Multiple simultaneous sessions handled correctly
- RLS policies prevent unauthorized data access
🔍 Code Quality Checks:
# Verify no deprecated packages grep -r "@supabase/auth-helpers" package.json # Must be 0 results # Verify correct package grep -r "@supabase/ssr" package.json # Must find it # Verify no mixed directives grep -l "'use client'" app/actions/*.ts # Must be 0 results grep -l "'use server'" app/\(auth\)/**/*.tsx # Must be 0 results # Verify RTL classes grep -r "px-" app/\(auth\) # Must be 0 results grep -r "pl-" app/\(auth\) # Must be 0 results grep -r "pr-" app/\(auth\) # Must be 0 results grep -r "text-left" app/\(auth\) # Must be 0 results
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.