# supabase-patterns > Best practices for Supabase with Next.js 14 and TypeScript. - Author: Mission Control - Repository: ascendantventures/av-command - Version: 20260201230819 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-06 - Source: https://github.com/ascendantventures/av-command - Web: https://mule.run/skillshub/@@ascendantventures/av-command~supabase-patterns:20260201230819 --- # Supabase Patterns Skill Best practices for Supabase with Next.js 14 and TypeScript. ## Typed Client Generation Always generate types from your database schema: ```bash npx supabase gen types typescript --project-id > src/lib/database.types.ts ``` Then create a typed client: ```typescript // src/lib/supabase.ts import { createClient } from '@supabase/supabase-js' import type { Database } from './database.types' export const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) ``` ## Insert/Upsert with Proper Types **WRONG** (causes build errors): ```typescript // ❌ Type inference fails const { data } = await supabase .from('tasks') .insert({ title, description }) ``` **CORRECT**: ```typescript // ✅ Explicit type assertion import type { Database } from '@/lib/database.types' type TaskInsert = Database['public']['Tables']['tasks']['Insert'] const newTask: TaskInsert = { title, description, priority: 'medium', } const { data, error } = await supabase .from('tasks') .insert(newTask) .select() .single() ``` ## Real-time Subscriptions ```typescript // Subscribe to changes const channel = supabase .channel('tasks-changes') .on( 'postgres_changes', { event: '*', schema: 'public', table: 'tasks' }, (payload) => { console.log('Change:', payload) } ) .subscribe() // Cleanup return () => { supabase.removeChannel(channel) } ``` ## Row Level Security (RLS) Always enable RLS on tables: ```sql -- Enable RLS ALTER TABLE tasks ENABLE ROW LEVEL SECURITY; -- Policy for authenticated users 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); ``` ## Server Components vs Client **Server Component** (for initial data): ```typescript // app/tasks/page.tsx import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' export default async function TasksPage() { const cookieStore = cookies() const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { get: (name) => cookieStore.get(name)?.value } } ) const { data: tasks } = await supabase.from('tasks').select() return } ``` **Client Component** (for real-time): ```typescript 'use client' import { useEffect, useState } from 'react' import { supabase } from '@/lib/supabase' export function TaskList({ initialTasks }) { const [tasks, setTasks] = useState(initialTasks) useEffect(() => { const channel = supabase .channel('tasks') .on('postgres_changes', { event: '*', schema: 'public', table: 'tasks' }, (payload) => { // Handle changes } ) .subscribe() return () => { supabase.removeChannel(channel) } }, []) return
{/* render tasks */}
} ``` ## Migrations Keep migrations in `supabase/migrations/`: ```sql -- supabase/migrations/20260201000000_create_tasks.sql CREATE TABLE tasks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, description TEXT, status TEXT NOT NULL DEFAULT 'backlog', priority TEXT NOT NULL DEFAULT 'medium', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Use gen_random_uuid() not uuid_generate_v4() -- The latter requires the uuid-ossp extension ``` ## Common Gotchas 1. **uuid_generate_v4() vs gen_random_uuid()**: Use `gen_random_uuid()` - it's built-in 2. **Missing types**: Always regenerate types after schema changes 3. **RLS blocking**: Check policies if queries return empty 4. **Real-time not firing**: Ensure table has REPLICA IDENTITY FULL