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
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
className="w-full px-4 py-2 border rounded"
/>
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
className="w-full px-4 py-2 border rounded"
/>
{error && (
{error}
)}
type="submit"
disabled={isLoading}
className="w-full py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{isLoading ? 'Signing in...' : 'Sign In'}
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
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
className="w-full px-4 py-2 border rounded"
/>
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
className="w-full px-4 py-2 border rounded"
/>
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password (min 6 characters)"
minLength={6}
required
className="w-full px-4 py-2 border rounded"
/>
{error && (
{error}
)}
type="submit"
disabled={isLoading}
className="w-full py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{isLoading ? 'Creating account...' : 'Create Account'}
);
}
```
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);
```