--- name: supabase-auth description: Implement Supabase authentication with SSR support. Email/password auth with automatic session management via cookies. Includes login, signup, email confirmation, and user profiles. license: MIT compatibility: TypeScript/JavaScript metadata: category: auth time: 4h source: drift-masterguide --- # Supabase Authentication Cookie-based authentication with SSR support. ## When to Use This Skill - Need authentication without rolling your own - Building a Next.js app with SSR - Want email/password + social auth options - Need automatic session refresh ## Core Concepts 1. **Browser client** - For client components 2. **Server client** - For API routes and server components 3. **Cookie-based sessions** - Automatic refresh via middleware 4. **User profiles** - Extended user data in your database ## TypeScript Implementation ### Browser Client ```typescript // lib/supabase.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 Client ```typescript // lib/supabase-server.ts import { createServerClient, type CookieOptions } from '@supabase/ssr'; import { cookies } from 'next/headers'; export async function createServerSupabaseClient() { const cookieStore = await cookies(); return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll(); }, setAll(cookiesToSet: { name: string; value: string; options: CookieOptions }[]) { try { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options) ); } catch { // Called from Server Component - ignore } }, }, } ); } ``` ### Login Page ```typescript // app/login/page.tsx 'use client'; import { useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { createClient } from '@/lib/supabase'; export default function LoginPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const router = useRouter(); const searchParams = useSearchParams(); const redirectTo = searchParams.get('redirectTo') || '/dashboard'; const supabase = createClient(); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setIsLoading(true); try { const { error } = await supabase.auth.signInWithPassword({ email, password, }); if (error) throw error; router.push(redirectTo); router.refresh(); } catch (err) { setError(err instanceof Error ? err.message : 'Login failed'); } finally { setIsLoading(false); } }; return (

Sign In

setEmail(e.target.value)} placeholder="Email" required className="w-full px-4 py-2 border rounded" /> setPassword(e.target.value)} placeholder="Password" required className="w-full px-4 py-2 border rounded" /> {error && (
{error}
)}

Don't have an account? Sign up

); } ``` ### Signup Page ```typescript // app/signup/page.tsx 'use client'; import { useState } from 'react'; import { createClient } from '@/lib/supabase'; export default function SignupPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [name, setName] = useState(''); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const [isLoading, setIsLoading] = useState(false); const handleSignup = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setIsLoading(true); try { const supabase = createClient(); const { data, error: signUpError } = await supabase.auth.signUp({ email, password, options: { data: { display_name: name }, emailRedirectTo: `${window.location.origin}/auth/callback`, }, }); if (signUpError) throw signUpError; if (data?.user?.identities?.length === 0) { setError('This email is already registered.'); return; } setSuccess(true); } catch (err) { setError(err instanceof Error ? err.message : 'Signup failed'); } finally { setIsLoading(false); } }; if (success) { return (

Check your email

We sent a confirmation link to {email}

); } return (

Create Account

setName(e.target.value)} placeholder="Name" className="w-full px-4 py-2 border rounded" /> setEmail(e.target.value)} placeholder="Email" required className="w-full px-4 py-2 border rounded" /> setPassword(e.target.value)} placeholder="Password (min 6 characters)" minLength={6} required className="w-full px-4 py-2 border rounded" /> {error && (
{error}
)}
); } ``` ### Auth Callback Route ```typescript // app/auth/callback/route.ts import { createServerClient } from '@supabase/ssr'; import { cookies } from 'next/headers'; import { NextResponse } from 'next/server'; export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url); const code = searchParams.get('code'); const next = searchParams.get('next') ?? '/dashboard'; if (code) { const cookieStore = await cookies(); const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll(); }, setAll(cookiesToSet) { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options) ); }, }, } ); const { error } = await supabase.auth.exchangeCodeForSession(code); if (!error) { return NextResponse.redirect(`${origin}${next}`); } } return NextResponse.redirect(`${origin}/login?error=auth_callback_error`); } ``` ### useUser Hook ```typescript // hooks/useUser.ts 'use client'; import { useEffect, useState, useCallback } from 'react'; import { createClient } from '@/lib/supabase'; import type { User } from '@supabase/supabase-js'; interface UserProfile { id: string; display_name: string | null; subscription_tier: 'free' | 'pro'; } export function useUser() { const [user, setUser] = useState(null); const [profile, setProfile] = useState(null); const [isLoading, setIsLoading] = useState(true); const supabase = createClient(); const fetchProfile = useCallback(async (userId: string) => { const { data } = await supabase .from('user_profiles') .select('*') .eq('id', userId) .single(); return data as UserProfile | null; }, [supabase]); const signOut = useCallback(async () => { await supabase.auth.signOut(); setUser(null); setProfile(null); }, [supabase]); useEffect(() => { const getSession = async () => { const { data: { session } } = await supabase.auth.getSession(); if (session?.user) { setUser(session.user); const profileData = await fetchProfile(session.user.id); setProfile(profileData); } setIsLoading(false); }; getSession(); const { data: { subscription } } = supabase.auth.onAuthStateChange( async (event, session) => { if (event === 'SIGNED_IN' && session?.user) { setUser(session.user); const profileData = await fetchProfile(session.user.id); setProfile(profileData); } else if (event === 'SIGNED_OUT') { setUser(null); setProfile(null); } } ); return () => subscription.unsubscribe(); }, [supabase, fetchProfile]); return { user, profile, tier: profile?.subscription_tier ?? 'free', isLoading, signOut, }; } ``` ### Database Migration ```sql -- migrations/001_user_profiles.sql CREATE TABLE user_profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, display_name VARCHAR(255), subscription_tier VARCHAR(20) NOT NULL DEFAULT 'free', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Auto-create profile on signup CREATE OR REPLACE FUNCTION handle_new_user() RETURNS TRIGGER AS $$ BEGIN INSERT INTO user_profiles (id, display_name) VALUES (NEW.id, NEW.raw_user_meta_data->>'display_name'); RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user(); -- RLS ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users can view own profile" ON user_profiles FOR SELECT USING (auth.uid() = id); CREATE POLICY "Users can update own profile" ON user_profiles FOR UPDATE USING (auth.uid() = id); ``` ## Best Practices 1. **Use SSR client** - Server components need cookie access 2. **Refresh in middleware** - Keep sessions alive automatically 3. **Auto-create profiles** - Database trigger on signup 4. **Enable RLS** - Row-level security on all user tables 5. **Handle email confirmation** - Check for empty identities array ## Common Mistakes - Using browser client in server components - Not refreshing session in middleware - Missing RLS policies on user data - Not handling email confirmation flow - Forgetting to call `router.refresh()` after login ## Related Skills - [Middleware Protection](../middleware-protection/) - [Row Level Security](../row-level-security/) - [JWT Auth](../jwt-auth/)