Next.js + Supabase Agent Rules
Project Context
- This is a full-stack web application using Next.js (App Router) with Supabase as the backend
- Supabase provides PostgreSQL database, authentication, storage, and real-time subscriptions
- Use TypeScript throughout; never write plain JavaScript files
- Target Next.js 14+ with the App Router and React Server Components by default
Code Style
- Use TypeScript strict mode; avoid `any` types — prefer `unknown` and narrow with type guards
- Define Zod schemas for all external data (API inputs, form data, Supabase responses)
- Use named exports for components and functions; default exports only for page/layout files
- Co-locate related code: keep hooks, utils, and types near the components that use them
- Format with Prettier and lint with ESLint (or Biome) on every commit
Project Structure
- Follow Next.js App Router conventions: `app/` for routes, `components/` for shared UI
- Group routes by feature using route groups: `(auth)`, `(dashboard)`, `(marketing)`
- Place Supabase client utilities in `lib/supabase/` with separate `client.ts` and `server.ts`
- Store database types in `lib/database.types.ts` (generated via `supabase gen types typescript`)
- Keep server actions in dedicated `actions.ts` files co-located with their routes
- Store Supabase migrations in `supabase/migrations/`
Supabase Client Setup
- Create separate Supabase clients for server and client contexts — never share instances
- Use `createServerClient` from `@supabase/ssr` in Server Components, Route Handlers, and Server Actions
- Use `createBrowserClient` from `@supabase/ssr` in Client Components
- Pass cookies correctly using the Next.js `cookies()` API on the server side
- Never expose the `service_role` key to the client; use it only in server-side code
Authentication
- Use Supabase Auth with `@supabase/ssr` for cookie-based session management
- Implement auth middleware in `middleware.ts` to refresh sessions on every request
- Protect server routes by checking `supabase.auth.getUser()` — never trust `getSession()` alone
- Use server actions for sign-in, sign-up, and sign-out flows
- Store user profile data in a separate `profiles` table linked by `auth.users.id`
- Always handle auth errors gracefully with user-friendly messages
Database & Typed Queries
- Always use generated types for Supabase queries — never use `any` for query results
- Prefer `.select()` with explicit column lists over `select('*')`
- Use RPC functions (`supabase.rpc()`) for complex queries that benefit from server-side SQL
- Use `.single()` when expecting exactly one row; handle the potential `null` result
- Apply `.order()`, `.limit()`, and `.range()` for pagination — never fetch unbounded result sets
- Run `supabase db diff` to generate migrations; never write DDL manually in production
Row Level Security
- Enable RLS on every table — no exceptions, even for tables that seem internal
- Write policies using `auth.uid()` to scope access to the authenticated user's data
- Use `USING` for SELECT/UPDATE/DELETE policies and `WITH CHECK` for INSERT/UPDATE
- Avoid overly permissive policies; default to deny and explicitly grant access
- Test RLS policies by querying as different user roles in the Supabase dashboard
Storage & File Uploads
- Create dedicated storage buckets with appropriate access policies (public vs private)
- Validate file types and sizes on both client and server before uploading
- Generate unique file paths using user ID and timestamps to avoid collisions
- Use signed URLs for private file access with short expiration times
- Clean up orphaned files when the associated database record is deleted
Real-time Subscriptions
- Use Supabase Realtime only in Client Components wrapped with proper cleanup
- Subscribe in `useEffect` and always return a cleanup function that removes the channel
- Filter subscriptions with `.on('postgres_changes', { filter: ... })` to minimize traffic
- Avoid subscribing to entire tables; filter by user ID or relevant foreign key
- Use `broadcast` for ephemeral events (typing indicators, presence) that do not need persistence
Testing
- Write unit tests for utility functions and validation schemas with Vitest
- Mock the Supabase client in tests using a factory function that returns typed stubs
- Use Playwright for end-to-end tests covering auth flows, CRUD operations, and real-time
- Test RLS policies separately by making queries with different user tokens
- Maintain a separate Supabase project or local instance for CI environments