Shai CLI
Sign in
Sign up
.cursorrules
# Next.js + Supabase Full-Stack Rules You are an expert in TypeScript, Next.js App Router, and Supabase (PostgreSQL, Auth, Storage, Edge Functions). ## Tech Stack - **Framework:** Next.js 14+ with App Router - **Database:** Supabase (PostgreSQL) - **Auth:** Supabase Auth with SSR - **Storage:** Supabase Storage - **Styling:** Tailwind CSS - **Type Safety:** TypeScript with generated types ## Project Structure ``` app/ ├── (auth)/ # Auth routes (login, signup) ├── (dashboard)/ # Protected routes ├── api/ # API routes (minimal, prefer Server Actions) ├── auth/callback/ # OAuth callback handler ├── layout.tsx └── page.tsx components/ ├── ui/ # Base UI components └── features/ # Feature-specific components lib/ ├── supabase/ │ ├── client.ts # Browser client │ ├── server.ts # Server client (cookies) │ ├── middleware.ts # Auth middleware helper │ └── admin.ts # Service role client (server only) ├── actions/ # Server Actions └── utils/ supabase/ ├── migrations/ # SQL migrations └── seed.sql # Seed data types/ └── database.ts # Generated types (npx supabase gen types) ``` ## Supabase Client Setup ```typescript // lib/supabase/client.ts - Browser client import { createBrowserClient } from '@supabase/ssr' import { Database } from '@/types/database' export const createClient = () => createBrowserClient<Database>( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) // lib/supabase/server.ts - Server client import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' import { Database } from '@/types/database' export const createClient = async () => { const cookieStore = await cookies() return createServerClient<Database>( 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) }) }, }, } ) } ``` ## Authentication Patterns ```typescript // middleware.ts - Refresh session on every request import { createServerClient } from '@supabase/ssr' import { NextResponse, type NextRequest } from 'next/server' export async function middleware(request: NextRequest) { let response = 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 }) => { response.cookies.set(name, value, options) }) }, }, } ) // Refresh session - IMPORTANT: don't remove this await supabase.auth.getUser() return response } export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], } ``` ## Database Queries ```typescript // Server Component - direct query async function PostsList() { const supabase = await createClient() const { data: posts, error } = await supabase .from('posts') .select(` id, title, created_at, author:profiles(name, avatar_url) `) .order('created_at', { ascending: false }) .limit(10) if (error) throw error return <ul>{posts.map(post => <li key={post.id}>{post.title}</li>)}</ul> } // Server Action - mutations 'use server' export async function createPost(formData: FormData) { const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() if (!user) throw new Error('Not authenticated') const { error } = await supabase .from('posts') .insert({ title: formData.get('title') as string, content: formData.get('content') as string, user_id: user.id, }) if (error) throw error revalidatePath('/posts') } ``` ## Row Level Security (RLS) Always enable RLS on tables. Example policies: ```sql -- Users can read all published posts CREATE POLICY "Public posts are viewable by everyone" ON posts FOR SELECT USING (published = true); -- Users can only insert their own posts CREATE POLICY "Users can insert their own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id); -- Users can only update their own posts CREATE POLICY "Users can update their own posts" ON posts FOR UPDATE USING (auth.uid() = user_id); -- Users can only delete their own posts CREATE POLICY "Users can delete their own posts" ON posts FOR DELETE USING (auth.uid() = user_id); ``` ## Real-time Subscriptions ```typescript 'use client' import { useEffect, useState } from 'react' import { createClient } from '@/lib/supabase/client' export function RealtimeMessages({ roomId }: { roomId: string }) { const [messages, setMessages] = useState<Message[]>([]) const supabase = createClient() useEffect(() => { // Initial fetch supabase .from('messages') .select('*') .eq('room_id', roomId) .order('created_at') .then(({ data }) => setMessages(data ?? [])) // Subscribe to changes const channel = supabase .channel(`room:${roomId}`) .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages', filter: `room_id=eq.${roomId}`, }, (payload) => setMessages(prev => [...prev, payload.new as Message]) ) .subscribe() return () => { supabase.removeChannel(channel) } }, [roomId, supabase]) return <ul>{messages.map(m => <li key={m.id}>{m.content}</li>)}</ul> } ``` ## Type Generation Regenerate types after schema changes: ```bash npx supabase gen types typescript --project-id <project-id> > types/database.ts ``` ## Security Rules - Never expose service role key to client - Always use RLS policies - Validate user input in Server Actions - Use parameterized queries (Supabase client does this automatically) - Check auth state in Server Actions before mutations ## Common Gotchas - Always call `await supabase.auth.getUser()` in middleware to refresh tokens - Use `revalidatePath` after mutations to update cached data - Don't forget to handle loading and error states - Use `select()` to specify columns - avoid `select('*')` in production - Remember RLS applies to service role only when `db.auth.uid()` is set
plaintext
Read only