# Complete Next.js + Supabase Development Guide 2025 **A comprehensive, standalone guide for building production-ready applications with Next.js 15 and Supabase** This guide covers everything from project setup to deployment, including authentication, database operations, TypeScript integration, and modern best practices. Perfect for beginners and as a reference for experienced developers. --- ## Table of Contents 1. [Prerequisites & Initial Setup](#prerequisites--initial-setup) 2. [Project Creation & Configuration](#project-creation--configuration) 3. [Supabase Integration](#supabase-integration) 4. [Project Structure](#project-structure) 5. [Database Schema & Security](#database-schema--security) 6. [Authentication Implementation](#authentication-implementation) 7. [CRUD Operations](#crud-operations) 8. [UI Components & Styling](#ui-components--styling) 9. [Error Handling & Loading States](#error-handling--loading-states) 10. [TypeScript Configuration](#typescript-configuration) 11. [Deployment & Production](#deployment--production) 12. [Complete Code Examples](#complete-code-examples) --- ## Prerequisites & Initial Setup ### System Requirements - **Node.js**: 18.17.0 or higher - **npm**: 9.0.0 or higher (or yarn/pnpm equivalent) - **Git**: For version control - **Code Editor**: VS Code recommended ### Version Requirements (2025) - **Next.js**: 15.x (latest) - **React**: 19 RC - **Supabase JS**: 2.50.1+ - **TypeScript**: 5.1.3+ - **Tailwind CSS**: 4.x ### Supabase Account Setup 1. Go to [supabase.com](https://supabase.com) 2. Sign up for a free account 3. Create a new project 4. Choose a database password (save it securely) 5. Wait for project initialization (~2 minutes) --- ## Project Creation & Configuration ### 1. Create Next.js Application ```bash npx create-next-app@latest my-supabase-app ``` **Choose these options when prompted:** ``` ✓ Would you like to use TypeScript? → Yes ✓ Would you like to use ESLint? → Yes ✓ Would you like to use Tailwind CSS? → Yes ✓ Would you like to use `src/` directory? → Yes ✓ Would you like to use App Router? → Yes ✓ Would you like to use Turbopack for `next dev`? → Yes ✓ Would you like to customize the default import alias (@/*)? → Yes ``` ### 2. Navigate to Project & Install Dependencies ```bash cd my-supabase-app npm install @supabase/supabase-js @supabase/ssr ``` **Important**: Don't install `@supabase/auth-helpers-nextjs` - it's deprecated. Use `@supabase/ssr` instead. ### 3. Environment Variables Setup Create `.env.local` in your project root: ```env # Supabase Configuration NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key # Optional: For admin operations (keep secure, never expose to client) SUPABASE_SERVICE_ROLE_KEY=your_service_role_key ``` **To find your Supabase credentials:** 1. Go to your Supabase project dashboard 2. Click on "Settings" → "API" 3. Copy "Project URL" and "anon public" key --- ## Supabase Integration ### 1. Client-Side Supabase Client Create `src/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! ) } ``` ### 2. Server-Side Supabase Client Create `src/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 { // Server component case - cookies can't be set } }, }, } ) } ``` ### 3. Session Management Middleware Create `middleware.ts` in your project root (same level as `package.json`): ```typescript import { type NextRequest } from 'next/server' 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)$).*)', ], } ``` Create `src/lib/supabase/middleware.ts`: ```typescript import { createServerClient } from '@supabase/ssr' import { NextResponse, type NextRequest } from 'next/server' export async function updateSession(request: NextRequest) { let supabaseResponse = NextResponse.next({ request, }) const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return request.cookies.getAll() }, setAll(cookiesToSet) { cookiesToSet.forEach(({ name, value, options }) => { request.cookies.set(name, value) supabaseResponse.cookies.set(name, value, options) }) }, }, } ) // Refresh session if needed const { data: { user } } = await supabase.auth.getUser() return supabaseResponse } ``` --- ## Project Structure ### Recommended File Organization ``` my-supabase-app/ ├── src/ │ ├── app/ # App Router (Next.js 13+) │ │ ├── globals.css # Global styles │ │ ├── layout.tsx # Root layout │ │ ├── page.tsx # Home page │ │ ├── loading.tsx # Global loading UI │ │ ├── error.tsx # Global error boundary │ │ ├── not-found.tsx # 404 page │ │ │ │ │ ├── (auth)/ # Route groups for organization │ │ │ ├── login/ │ │ │ │ ├── page.tsx │ │ │ │ ├── actions.ts # Server actions │ │ │ │ └── loading.tsx │ │ │ └── signup/ │ │ │ └── page.tsx │ │ │ │ │ ├── dashboard/ # Protected routes │ │ │ ├── layout.tsx # Dashboard layout │ │ │ ├── page.tsx │ │ │ ├── actions.ts # Server actions for dashboard │ │ │ ├── loading.tsx │ │ │ ├── error.tsx │ │ │ └── tasks/ │ │ │ └── page.tsx │ │ │ │ │ └── api/ # API routes │ │ └── auth/ │ │ └── confirm/ │ │ └── route.ts # Email confirmation │ │ │ ├── components/ # Reusable components │ │ ├── ui/ # Basic UI components (shadcn/ui) │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── input.tsx │ │ │ └── skeleton.tsx │ │ ├── layout/ # Layout components │ │ │ ├── header.tsx │ │ │ ├── nav.tsx │ │ │ └── footer.tsx │ │ └── features/ # Feature-specific components │ │ ├── auth/ │ │ │ ├── login-form.tsx │ │ │ └── logout-button.tsx │ │ └── tasks/ │ │ ├── task-list.tsx │ │ └── task-form.tsx │ │ │ ├── lib/ # Utility libraries │ │ ├── utils.ts # General utilities │ │ ├── dal.ts # Data Access Layer (security) │ │ ├── validations.ts # Schema validations │ │ └── supabase/ # Supabase configuration │ │ ├── client.ts # Client-side client │ │ ├── server.ts # Server-side client │ │ └── middleware.ts # Session management │ │ │ ├── hooks/ # Custom React hooks │ │ ├── use-auth.ts │ │ └── use-local-storage.ts │ │ │ └── types/ # TypeScript definitions │ └── database.types.ts # Generated Supabase types │ ├── middleware.ts # Next.js middleware ├── next.config.ts # Next.js configuration ├── tailwind.config.ts # Tailwind configuration ├── tsconfig.json # TypeScript configuration └── package.json ``` --- ## Database Schema & Security ### 1. Create Tables in Supabase Go to your Supabase project → SQL Editor → New Query, and run: ```sql -- Create the tasks table CREATE TABLE tasks ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, title TEXT NOT NULL, description TEXT, completed BOOLEAN DEFAULT FALSE, user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL ); -- Enable Row Level Security (RLS) ALTER TABLE tasks ENABLE ROW LEVEL SECURITY; -- Create security policies CREATE POLICY "Users can view own tasks" ON tasks FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "Users can insert own tasks" ON tasks FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY "Users can update own tasks" ON tasks FOR UPDATE USING (auth.uid() = user_id); CREATE POLICY "Users can delete own tasks" ON tasks FOR DELETE USING (auth.uid() = user_id); -- Create indexes for performance CREATE INDEX idx_tasks_user_id ON tasks(user_id); CREATE INDEX idx_tasks_created_at ON tasks(created_at DESC); -- Add updated_at trigger CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = TIMEZONE('utc'::text, NOW()); RETURN NEW; END; $$ language 'plpgsql'; CREATE TRIGGER update_tasks_updated_at BEFORE UPDATE ON tasks FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` ### 2. Generate TypeScript Types Run this command to generate types from your database: ```bash npx supabase gen types typescript --project-id your_project_id > src/types/database.types.ts ``` Replace `your_project_id` with your actual Supabase project ID (found in Project Settings → General). --- ## Authentication Implementation ### 1. Data Access Layer (Security) Create `src/lib/dal.ts`: ```typescript import 'server-only' import { redirect } from 'next/navigation' import { createClient } from '@/lib/supabase/server' import { cache } from 'react' export const verifySession = cache(async () => { const supabase = await createClient() const { data: { user }, error } = await supabase.auth.getUser() if (error || !user) { redirect('/login') } return { isAuth: true, user } }) export const getUser = cache(async () => { const session = await verifySession() if (!session) return null return session.user }) ``` ### 2. Login Page Create `src/app/(auth)/login/page.tsx`: ```typescript import { login, signup } from './actions' export default function LoginPage() { return (

Sign in to your account

) } ``` ### 3. Authentication Server Actions Create `src/app/(auth)/login/actions.ts`: ```typescript 'use server' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' import { createClient } from '@/lib/supabase/server' export async function login(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) { console.error('Login error:', error) redirect('/login?error=Invalid credentials') } revalidatePath('/', 'layout') redirect('/dashboard') } export async function signup(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.signUp(data) if (error) { console.error('Signup error:', error) redirect('/login?error=Could not create account') } revalidatePath('/', 'layout') redirect('/login?message=Check your email to confirm your account') } export async function logout() { const supabase = await createClient() await supabase.auth.signOut() revalidatePath('/', 'layout') redirect('/login') } ``` ### 4. Email Confirmation Route Create `src/app/api/auth/confirm/route.ts`: ```typescript import { createClient } from '@/lib/supabase/server' import { NextRequest, NextResponse } from 'next/server' export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const token_hash = searchParams.get('token_hash') const type = searchParams.get('type') const next = searchParams.get('next') ?? '/dashboard' if (token_hash && type) { const supabase = await createClient() const { error } = await supabase.auth.verifyOtp({ type: type as any, token_hash, }) if (!error) { return NextResponse.redirect(new URL(next, request.url)) } } // Redirect to error page if confirmation fails return NextResponse.redirect(new URL('/login?error=Could not confirm account', request.url)) } ``` --- ## CRUD Operations ### 1. Dashboard Page with Data Fetching Create `src/app/dashboard/page.tsx`: ```typescript import { verifySession } from '@/lib/dal' import { createClient } from '@/lib/supabase/server' import TaskList from '@/components/features/tasks/task-list' import TaskForm from '@/components/features/tasks/task-form' import LogoutButton from '@/components/features/auth/logout-button' export default async function Dashboard() { const session = await verifySession() const supabase = await createClient() const { data: tasks, error } = await supabase .from('tasks') .select('*') .order('created_at', { ascending: false }) if (error) { console.error('Error fetching tasks:', error) return
Error loading tasks
} return (

Dashboard

Welcome back, {session.user.email}

) } ``` ### 2. Server Actions for CRUD Operations Create `src/app/dashboard/actions.ts`: ```typescript 'use server' import { createClient } from '@/lib/supabase/server' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' export async function createTask(formData: FormData) { const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() if (!user) redirect('/login') const title = formData.get('title') as string const description = formData.get('description') as string if (!title.trim()) { return { error: 'Title is required' } } const { error } = await supabase .from('tasks') .insert([ { title: title.trim(), description: description?.trim() || null, user_id: user.id, }, ]) if (error) { console.error('Error creating task:', error) return { error: 'Failed to create task' } } revalidatePath('/dashboard') return { success: true } } export async function updateTask(id: string, formData: FormData) { const supabase = await createClient() const title = formData.get('title') as string const description = formData.get('description') as string const completed = formData.get('completed') === 'on' const { error } = await supabase .from('tasks') .update({ title: title.trim(), description: description?.trim() || null, completed, }) .eq('id', id) if (error) { console.error('Error updating task:', error) return { error: 'Failed to update task' } } revalidatePath('/dashboard') return { success: true } } export async function deleteTask(id: string) { const supabase = await createClient() const { error } = await supabase .from('tasks') .delete() .eq('id', id) if (error) { console.error('Error deleting task:', error) return { error: 'Failed to delete task' } } revalidatePath('/dashboard') return { success: true } } export async function toggleTask(id: string, completed: boolean) { const supabase = await createClient() const { error } = await supabase .from('tasks') .update({ completed }) .eq('id', id) if (error) { console.error('Error toggling task:', error) return { error: 'Failed to update task' } } revalidatePath('/dashboard') return { success: true } } ``` --- ## UI Components & Styling ### 1. Utility Functions Create `src/lib/utils.ts`: ```typescript import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } export function formatDate(date: string | Date) { return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }).format(new Date(date)) } ``` ### 2. Task List Component Create `src/components/features/tasks/task-list.tsx`: ```typescript import { toggleTask, deleteTask } from '@/app/dashboard/actions' import { formatDate } from '@/lib/utils' type Task = { id: string title: string description: string | null completed: boolean created_at: string updated_at: string } type TaskListProps = { tasks: Task[] } export default function TaskList({ tasks }: TaskListProps) { if (tasks.length === 0) { return (

No tasks yet. Create your first task!

) } return (

Your Tasks

{tasks.map((task) => (

{task.title}

{task.description && (

{task.description}

)}

Created {formatDate(task.created_at)}

))}
) } ``` ### 3. Task Form Component Create `src/components/features/tasks/task-form.tsx`: ```typescript 'use client' import { createTask } from '@/app/dashboard/actions' import { useActionState } from 'react' export default function TaskForm() { const [state, formAction, pending] = useActionState(createTask, null) return (

Add New Task