{"filename":"nextjs15-supabase-cursorrules-prompt-file.mdc","content":"---\ndescription: \"27 architecture rules preventing AI hallucinations: insecure auth (getSession vs getUser), synchronous params, deprecated imports, missing RLS, and Stripe key exposure. Built for Cursor Agent and Claude Code.\"\nglobs: **/*\nalwaysApply: false\n---\n# Next.js 15 + Supabase Architecture Rules\n\nYou are an expert Next.js 15 developer working with Supabase, TypeScript (strict), and shadcn/ui.\nFollow ALL rules below unconditionally. If you are tempted to deviate, re-read the rule.\n\n## Tech Stack\n- Framework: Next.js 15 (App Router) with React 19\n- Language: TypeScript (strict mode)\n- Styling: Tailwind CSS + shadcn/ui\n- Database: Supabase (PostgreSQL + RLS)\n- Auth: Supabase SSR (cookie-based, @supabase/ssr)\n- Validation: Zod\n- Payments: Stripe (server-side only)\n\n## RULE 1: NEVER use getSession() on the server\n\nSECURITY CRITICAL. getSession() reads the JWT from cookies WITHOUT verifying it.\nA forged cookie passes silently. ALWAYS use getUser() for server-side auth.\n\n```typescript\n// ✅ CORRECT — verified with Supabase auth server\nconst supabase = await createClient()\nconst { data: { user } } = await supabase.auth.getUser()\nif (!user) redirect('/login')\n\n// ❌ WRONG — reads JWT without verification, session can be forged\nconst { data: { session } } = await supabase.auth.getSession()\n```\n\n## RULE 2: NEVER access params synchronously in Next.js 15\n\nIn Next.js 15, params and searchParams are Promises. Synchronous access compiles\nbut crashes at runtime.\n\n```typescript\n// ✅ CORRECT\nexport default async function Page({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params\n}\n\n// ❌ WRONG — runtime crash\nexport default function Page({ params }: { params: { id: string } }) {\n  const { id } = params // TypeError at runtime\n}\n```\n\n## RULE 3: NEVER import from @supabase/auth-helpers-nextjs\n\nThis package is deprecated. It does NOT work with Next.js 15 App Router cookies.\nALWAYS use @supabase/ssr with manual cookie handling.\n\n```typescript\n// ✅ CORRECT\nimport { createServerClient } from '@supabase/ssr'\n\n// ❌ WRONG — deprecated, broken with App Router\nimport { createServerComponentClient } from '@supabase/auth-helpers-nextjs'\n```\n\n## RULE 4: All database tables MUST have RLS enabled\n\nEvery Supabase table must have Row Level Security enabled. Without RLS, any\nuser with the anon key can read ALL data from the table.\n\n```sql\n-- ✅ Always add after CREATE TABLE:\nALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY;\n\nCREATE POLICY \"Users can only read their own data\"\n  ON public.my_table FOR SELECT\n  USING (auth.uid() = user_id);\n```\n\n## RULE 5: Default to Server Components\n\nOnly add 'use client' when the component needs interactivity (event handlers,\nuseState, useEffect). Push 'use client' to the smallest leaf component possible.\n\n## RULE 6: All mutations via Server Actions\n\nAll data mutations happen through Server Actions, never client-side fetch().\nAlways validate with Zod, authenticate with getUser(), and return ActionResponse<T>.\n\n```typescript\n'use server'\nimport { z } from 'zod'\n\ntype ActionResponse<T = void> =\n  | { success: true; data: T }\n  | { success: false; error: string }\n\nconst Schema = z.object({ title: z.string().min(1).max(200) })\n\nexport async function createItem(input: unknown): Promise<ActionResponse> {\n  const supabase = await createClient()\n  const { data: { user } } = await supabase.auth.getUser()\n  if (!user) return { success: false, error: 'Unauthorized' }\n\n  const result = Schema.safeParse(input)\n  if (!result.success) return { success: false, error: 'Invalid input' }\n\n  const { error } = await supabase.from('items').insert({ ...result.data, user_id: user.id })\n  if (error) return { success: false, error: 'Failed to create item' }\n\n  revalidatePath('/items')\n  return { success: true, data: undefined }\n}\n```\n\n## RULE 7: Error boundaries for every data-fetching page\n\nEvery page that fetches data MUST have sibling loading.tsx and error.tsx files.\n\n## RULE 8: NEVER expose Stripe secret keys\n\nStripe keys starting with `sk_` must NEVER be in NEXT_PUBLIC_ variables.\nUse process.env.STRIPE_SECRET_KEY (server-only).\n\n## RULE 9: TypeScript strict mode, no `any`\n\nNEVER use `any`. Use `unknown` with Zod validation or type narrowing.\ntsconfig.json MUST have `strict: true`.\n\n## RULE 10: Middleware is for session REFRESH only\n\nNEVER put auth enforcement in Next.js middleware. Middleware runs on Edge Runtime\nand cannot verify Supabase JWTs. Auth enforcement belongs in layouts/pages with getUser().\n\n---\n\nFull rule set (27 rules) available at: https://github.com/vibestackdev/vibe-stack\nQuick install: npx vibe-stack-rules init\n"}