--- name: supabase-fullstack-setup description: Complete guide to integrating Supabase with Next.js 13+ App Router, including authentication, database setup, Row Level Security (RLS), and common patterns for production apps. Use when starting a new Next.js project with Supabase, need authentication with social providers, or building apps with user-specific data. --- # Supabase Full-Stack Setup for Next.js Complete guide to integrating Supabase with Next.js 13+ App Router, including authentication, database setup, Row Level Security (RLS), and common patterns for production apps. ## When to use this skill - Starting a new Next.js project with Supabase - Need authentication with social providers - Building apps with user-specific data (notes, favorites, etc.) - Want real-time subscriptions - Need secure server-side and client-side data access - Implementing rate limiting or audit logs with database - Building SaaS applications ## Core Setup ### Step 1: Install Dependencies ```bash npm install @supabase/supabase-js @supabase/ssr # or pnpm add @supabase/supabase-js @supabase/ssr ``` ### Step 2: Environment Variables Create `.env.local`: ```bash NEXT_PUBLIC_SUPABASE_URL=your-project-url NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key ``` Get these from: https://app.supabase.com/project/_/settings/api ### Step 3: Create Supabase Clients **Server Client** (`lib/supabase/server.ts`): ```typescript import { createServerClient } 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: { getAll() { return cookieStore.getAll() }, setAll(cookiesToSet) { try { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options) ) } catch { // Called from Server Component - middleware will handle } }, }, } ) } ``` **Browser Client** (`lib/supabase/client.ts`): ```typescript import { createBrowserClient } from '@supabase/ssr' export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) } ``` ### Step 4: Authentication Setup **Auth Context** (`contexts/auth-context.tsx`): ```typescript 'use client'; import { createContext, useContext, useEffect, useState } from 'react'; import { createClient } from '@/lib/supabase/client'; import type { User } from '@supabase/supabase-js'; interface AuthContextType { user: User | null; loading: boolean; signIn: (email: string, password: string) => Promise; signUp: (email: string, password: string) => Promise; signOut: () => Promise; } const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const supabase = createClient(); useEffect(() => { // Get initial session supabase.auth.getSession().then(({ data: { session } }) => { setUser(session?.user ?? null); setLoading(false); }); // Listen for auth changes const { data: { subscription }, } = supabase.auth.onAuthStateChange((_event, session) => { setUser(session?.user ?? null); }); return () => subscription.unsubscribe(); }, []); const signIn = async (email: string, password: string) => { const { error } = await supabase.auth.signInWithPassword({ email, password, }); if (error) throw error; }; const signUp = async (email: string, password: string) => { const { error} = await supabase.auth.signUp({ email, password, }); if (error) throw error; }; const signOut = async () => { const { error } = await supabase.auth.signOut(); if (error) throw error; }; return ( {children} ); } export const useAuth = () => { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within AuthProvider'); } return context; }; ``` ### Step 5: Database Schema Examples **Video Analysis Table**: ```sql CREATE TABLE video_analyses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), youtube_id TEXT NOT NULL UNIQUE, user_id UUID REFERENCES auth.users(id), title TEXT NOT NULL, author TEXT, thumbnail_url TEXT, duration INTEGER, transcript JSONB, topics JSONB, summary TEXT, suggested_questions JSONB, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_video_analyses_youtube_id ON video_analyses(youtube_id); CREATE INDEX idx_video_analyses_user_id ON video_analyses(user_id); ``` **Notes Table**: ```sql CREATE TABLE notes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id) NOT NULL, video_id UUID REFERENCES video_analyses(id) ON DELETE CASCADE, source TEXT NOT NULL, source_id TEXT, text TEXT NOT NULL, metadata JSONB, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_notes_user_id ON notes(user_id); CREATE INDEX idx_notes_video_id ON notes(video_id); ``` **Favorites Table**: ```sql CREATE TABLE user_favorites ( user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, video_analysis_id UUID REFERENCES video_analyses(id) ON DELETE CASCADE, created_at TIMESTAMPTZ DEFAULT NOW(), PRIMARY KEY (user_id, video_analysis_id) ); ``` **Rate Limiting Table**: ```sql CREATE TABLE rate_limits ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), key TEXT NOT NULL, identifier TEXT NOT NULL, timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_rate_limits_key_timestamp ON rate_limits(key, timestamp); CREATE INDEX idx_rate_limits_timestamp ON rate_limits(timestamp); ``` ### Step 6: Row Level Security (RLS) ```sql -- Enable RLS ALTER TABLE notes ENABLE ROW LEVEL SECURITY; ALTER TABLE user_favorites ENABLE ROW LEVEL SECURITY; -- Notes policies CREATE POLICY "Users can view their own notes" ON notes FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "Users can create their own notes" ON notes FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY "Users can update their own notes" ON notes FOR UPDATE USING (auth.uid() = user_id); CREATE POLICY "Users can delete their own notes" ON notes FOR DELETE USING (auth.uid() = user_id); -- Favorites policies CREATE POLICY "Users can view their own favorites" ON user_favorites FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "Users can add favorites" ON user_favorites FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY "Users can remove favorites" ON user_favorites FOR DELETE USING (auth.uid() = user_id); ``` ## Usage Examples ### Example 1: Server-Side Data Fetching ```typescript // app/my-notes/page.tsx import { createClient } from '@/lib/supabase/server'; export default async function MyNotesPage() { const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) { redirect('/login'); } const { data: notes } = await supabase .from('notes') .select('*') .eq('user_id', user.id) .order('created_at', { ascending: false }); return (

My Notes

{notes?.map(note => (
{note.text}
))}
); } ``` ### Example 2: Client-Side Mutations ```typescript 'use client'; import { createClient } from '@/lib/supabase/client'; import { useAuth } from '@/contexts/auth-context'; export function CreateNoteForm({ videoId }: { videoId: string }) { const { user } = useAuth(); const supabase = createClient(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const text = formData.get('text') as string; const { error } = await supabase .from('notes') .insert({ user_id: user!.id, video_id: videoId, source: 'custom', text }); if (error) { console.error('Error creating note:', error); } }; return (