CursorPool
← 返回首页

Vibe Stack

Production-grade .mdc rules that prevent Cursor from hallucinating insecure auth, deprecated Supabase packages, and broken Next.js 15 patterns. Drop into any project in 30 seconds.

cursor.directory·8
规则

Instructions for AI agent code generation workflow

Instructions for AI agent code generation workflow

# AI Collaboration Guidelines

The 3-stage agentic loop:

1. **PLAN:** 
"Analyze the current codebase and plan how to implement [feature]. List the files you'll modify and why. Don't write code yet."

2. **IMPLEMENT:** 
"Implement the plan. Follow all `.cursor/rules/` guidelines. If you're unsure about auth or RLS, check `supabase-auth-security.mdc`."

3. **REVIEW:** 
"Review what you just generated. Check for:
- `getSession()` usage (MUST be `getUser()`)
- Missing error boundaries or `loading.tsx`
- Next.js 15 async layout/page params
- TypeScript `any` types (use `unknown` or define interface)
Report any issues before I run it."

Constraints:
- NEVER give the AI a task longer than 200 words.
- NEVER let the AI implement more than 3-4 files in one turn.
- ALWAYS review the Git diff before committing.
规则

API route handler design patterns

API route handler design patterns

# API Design Patterns

Standard response format for Route Handlers:
Success: `{ data: T, error: null }`
Error:   `{ data: null, error: { code: string, message: string } }`

HTTP status codes:
- 200: Success (GET, PATCH)
- 201: Created (POST)
- 204: No content (DELETE)
- 400: Validation error
- 401: Unauthenticated
- 403: Unauthorized  
- 404: Not found
- 500: Server error

Route handler template:
```typescript
import { NextResponse } from 'next/server'
import { createServerClient } from '@supabase/ssr'

export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const supabase = createServerClient(...)
  
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) {
    return NextResponse.json(
      { data: null, error: { code: 'UNAUTHORIZED', message: 'Login required' } }, 
      { status: 401 }
    )
  }
  
  // ... fetch data
  return NextResponse.json({ data: { id, status: 'ok' }, error: null })
}
```
规则

Next.js caching and revalidation best practices

Next.js caching and revalidation best practices

# Caching & Revalidation

## Default Behavior
Next.js 15 has specific caching defaults. AI models often get these wrong.

## Rules

### Server Components - Data Fetching
For dynamic data that changes frequently:
```typescript
// Force fresh data on every request
const data = await fetch(url, { cache: 'no-store' })

// Or revalidate every 60 seconds:
const data = await fetch(url, { next: { revalidate: 60 } })
```

### Route Segment Config
For pages that must always show fresh data:
```typescript
// At the top of page.tsx or layout.tsx:
export const dynamic = 'force-dynamic'  // Never cache this page
export const revalidate = 0             // Same as above
```

For static pages that rarely change:
```typescript
export const revalidate = 3600  // Revalidate every hour
```

### Server Actions — Always Revalidate
After mutations, ALWAYS revalidate affected paths:
```typescript
'use server'
import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  // ... create post in database
  revalidatePath('/posts')  // ← NEVER forget this
  revalidatePath('/dashboard')  // ← Revalidate all affected pages
}
```

### Anti-Patterns
- NEVER assume `fetch()` will always return fresh data — check your cache settings
- NEVER forget `revalidatePath()` after a mutation in a Server Action
- NEVER use `cache: 'force-cache'` for user-specific data
- NEVER rely on client-side state to show "updated" data after a mutation — revalidate the server cache
- ALWAYS use `revalidateTag()` for fine-grained cache invalidation when possible
规则

Database schema design patterns for Supabase with proper types and relationships

Database schema design patterns for Supabase with proper types and relationships

# Database Design Patterns

## Table Naming
- Use `snake_case` for tables and columns: `user_profiles`, `created_at`
- NEVER use camelCase in SQL: `userId` → WRONG, `user_id` → CORRECT
- Use plural table names: `profiles`, `posts`, `comments`

## Required Columns (Every Table)
```sql
CREATE TABLE public.example (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  -- your columns here
  created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
  updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);
```
ALWAYS include `id`, `created_at`, and `updated_at`.
ALWAYS use UUID for primary keys (not serial/integer).
ALWAYS use TIMESTAMPTZ (not TIMESTAMP) for timezone safety.

## Foreign Key Pattern
```sql
-- Always reference auth.users for user ownership
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL
```
Use `ON DELETE CASCADE` for user-owned data.
Use `ON DELETE SET NULL` for optional relationships.

## RLS Template (Copy This Every Time)
```sql
ALTER TABLE public.example ENABLE ROW LEVEL SECURITY;

-- Users can only see their own data
CREATE POLICY "Users own data" ON public.example
  FOR ALL USING (auth.uid() = user_id);

-- Or for public read + owner write:
CREATE POLICY "Public read" ON public.example
  FOR SELECT USING (true);
CREATE POLICY "Owner write" ON public.example
  FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Owner update" ON public.example
  FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Owner delete" ON public.example
  FOR DELETE USING (auth.uid() = user_id);
```

## Type Generation
After schema changes, regenerate types:
```bash
npx supabase gen types typescript --project-id YOUR_PROJECT_REF > src/types/database.ts
```
ALWAYS use generated types — NEVER manually type database schemas.

## Anti-Patterns
- NEVER use `TEXT` for fields that should be enums — use PostgreSQL enums or CHECK constraints
- NEVER store JSON blobs when structured columns work — use `JSONB` only for truly dynamic data
- NEVER create tables without RLS — see `supabase-rls.mdc`
- NEVER use `SERIAL` for IDs — use `UUID` for security (prevents enumeration attacks)
规则

Environment variable management and secrets handling

Environment variable management and secrets handling

# Environment Variable Management

## Classification Rules
| Prefix | Visibility | Use For |
|--------|-----------|---------|
| `NEXT_PUBLIC_` | Client + Server | Non-sensitive public values (Supabase URL, Stripe publishable key) |
| No prefix | Server Only | ALL secrets (API keys, webhook secrets, database URLs) |

## NEVER Expose These to the Client
```
STRIPE_SECRET_KEY         → Server only (NEVER NEXT_PUBLIC_)
STRIPE_WEBHOOK_SECRET     → Server only
SUPABASE_SERVICE_ROLE_KEY → Server only (admin bypass key)
RESEND_API_KEY            → Server only
DATABASE_URL              → Server only
```

## Safe for Client
```
NEXT_PUBLIC_SUPABASE_URL           → Public Supabase endpoint
NEXT_PUBLIC_SUPABASE_ANON_KEY      → Rate-limited client key (RLS protects data)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY → Stripe public key (payment form only)
NEXT_PUBLIC_APP_URL                → Your app's URL
```

## Validation Pattern
Use `src/lib/env.ts` to validate at startup:
```typescript
function getEnvVar(key: string, required = true): string {
  const value = process.env[key]
  if (!value && required) {
    throw new Error(`❌ Missing required env var: ${key}`)
  }
  return value ?? ''
}
```

## .env.local Structure
```bash
# Supabase (required)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...

# Stripe (optional in dev)
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

# Email (optional in dev)
RESEND_API_KEY=re_...

# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
```

## Anti-Patterns
- NEVER commit `.env.local` — it's in `.gitignore`
- NEVER use `process.env.X` directly in components — use the centralized `env` config
- NEVER hardcode API keys or secrets in source code
- NEVER use `NEXT_PUBLIC_` for write-access API keys
规则

Error boundaries and loading states guidelines

Error boundaries and loading states guidelines

# Error Handling and Loading States

EVERY page in `app/` router MUST have sibling files if it does data fetching:
- `loading.tsx` (Suspense boundary)
- `error.tsx` (Error boundary — must be `'use client'`)

`loading.tsx` template:
```tsx
export default function Loading() {
  return <div className="animate-pulse">Loading...</div>
}
```

`error.tsx` template:
```tsx
'use client'
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div>
      <h2>Something went wrong</h2>
      <button onClick={reset}>Try again</button>
    </div>
  )
}
```

Server Action error pattern:
```typescript
export async function createItem(formData: FormData) {
  try {
    // ... operation
    return { success: true }
  } catch (error) {
    return { error: 'Failed to create item' }
  }
}
```
规则

File naming conventions and project organization rules

File naming conventions and project organization rules

# File Naming & Organization

## Naming Conventions
- **Components:** PascalCase → `UserProfile.tsx`, `DashboardLayout.tsx`
- **Utilities/Libs:** camelCase → `formatDate.ts`, `createClient.ts`
- **Constants:** SCREAMING_SNAKE → `export const MAX_RETRIES = 3`
- **Types files:** camelCase → `types/index.ts`, `types/database.ts`
- **Server Actions:** camelCase in grouped files → `actions.ts` (not one file per action)
- **Route Handlers:** always `route.ts` (Next.js convention)

## Directory Rules
- Components in `src/components/` — NEVER in `src/app/`
- Page-specific components in `src/app/[route]/_components/` (underscore prefix = private)
- Shared types in `src/types/` — NEVER define types inline in components
- Business logic in `src/lib/` — NEVER put business logic in components
- Server actions in the nearest `actions.ts` to where they're used

## Import Order (enforce in every file)
```typescript
// 1. React/Next.js imports
import { Suspense } from 'react'
import Link from 'next/link'

// 2. Third-party libraries
import { z } from 'zod'

// 3. Internal aliases (@/)
import { Button } from '@/components/ui/button'
import { createClient } from '@/lib/supabase/server'
import type { UserProfile } from '@/types'

// 4. Relative imports (only for co-located files)
import { columns } from './columns'
```

## Anti-Patterns
- NEVER create `utils/helpers.ts` catch-all files — name files by what they do
- NEVER put more than 1 exported component per file
- NEVER use default exports for utilities — use named exports
- NEVER create `index.ts` barrel files that re-export everything (breaks tree-shaking)
规则

Git workflow and commit conventions for AI-assisted development

Git workflow and commit conventions for AI-assisted development

# Git Conventions for Vibe Coders

Commit convention (Conventional Commits):
- `feat:` add user authentication
- `fix:` resolve RLS policy for shared projects  
- `chore:` update dependencies
- `docs:` add API documentation
- `refactor:` extract auth helper to separate module

Branch naming:
- `feature/auth-flow`
- `fix/rls-policy-bug`
- `chore/update-dependencies`

ALWAYS run before commit (if available):
1. `npm run lint`
2. `npm run type-check`
3. `npm test`

AI session workflow:
- Create a new branch before starting any AI session
- Review ALL AI-generated code before committing
- Break large AI generations into multiple focused commits
规则

React hydration mismatch prevention

React hydration mismatch prevention

# Hydration Mismatch Prevention

## The Problem
Hydration mismatches occur when server-rendered HTML doesn't match the client-rendered output, causing UI flickers, errors, and broken interactivity.

## Common AI Mistakes

### 1. Non-deterministic Values in Render
❌ WRONG — different value on server vs client:
```tsx
export default function Component() {
  const id = Math.random()  // Different on server vs client!
  return <div id={`item-${id}`}>Content</div>
}
```

✅ CORRECT — use useId() for unique IDs:
```tsx
import { useId } from 'react'
export default function Component() {
  const id = useId()
  return <div id={id}>Content</div>
}
```

### 2. Browser APIs in Server-Rendered Code
❌ WRONG — window/document don't exist on server:
```tsx
export default function Component() {
  const width = window.innerWidth  // ReferenceError on server!
  return <div>{width}</div>
}
```

✅ CORRECT — guard with useEffect:
```tsx
'use client'
import { useState, useEffect } from 'react'
export default function Component() {
  const [width, setWidth] = useState(0)
  useEffect(() => { setWidth(window.innerWidth) }, [])
  return <div>{width}</div>
}
```

### 3. Date/Time Rendering
❌ WRONG — different timezone on server vs client:
```tsx
export default function Component() {
  return <p>{new Date().toLocaleString()}</p>  // Hydration mismatch!
}
```

✅ CORRECT — render dates client-side or use consistent formatting:
```tsx
'use client'
import { useState, useEffect } from 'react'
export default function DateDisplay({ date }: { date: string }) {
  const [formatted, setFormatted] = useState(date)
  useEffect(() => { setFormatted(new Date(date).toLocaleString()) }, [date])
  return <p>{formatted}</p>
}
```

## Rule
- NEVER use Math.random(), crypto.randomUUID(), or Date.now() directly in render
- NEVER access window, document, localStorage, or navigator without checking typeof window !== 'undefined'
- Use React.useId() for dynamically generated IDs
- Wrap browser-only logic in useEffect
规则

Critical Next.js 15 breaking changes regarding async params and searchParams

Critical Next.js 15 breaking changes regarding async params and searchParams

# Next.js 15 Async Params (BREAKING CHANGE)

CRITICAL: In Next.js 15+, `params` and `searchParams` are ASYNC Promises.
This is a breaking change from Next.js 14. The AI MUST await them.

## Rule: Always Use Promise Types

✅ CORRECT (Next.js 15):
```tsx
type PageProps = { 
  params: Promise<{ slug: string }> 
  searchParams: Promise<{ [key: string]: string | string[] | undefined }> 
}

export default async function Page({ params, searchParams }: PageProps) {
  const { slug } = await params;
  const search = await searchParams;
}
```

❌ WRONG (outdated Next.js 14 pattern — will crash silently):
```tsx
// DO NOT GENERATE THIS PATTERN
export default function Page({ params }: { params: { slug: string } }) {
  const { slug } = params; // BREAKS: params is a Promise in Next.js 15
}
```

## Rule: generateMetadata Also Uses Async Params
```tsx
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  return { title: `Post: ${slug}` }
}
```

## Rule: Layout Components MUST Await Params
```tsx
export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Promise<{ teamId: string }>
}) {
  const { teamId } = await params;
  return <div>{children}</div>
}
```
规则

Next.js performance optimization — caching, bundle size, and Core Web Vitals

Next.js performance optimization — caching, bundle size, and Core Web Vitals

# Performance & Caching

## Data Fetching Rules
1. Use parallel fetching with `Promise.all()` when data is independent:
```tsx
// ✅ CORRECT: Parallel — both requests fire simultaneously
const [user, posts] = await Promise.all([
  getUser(userId),
  getPosts(userId),
])

// ❌ WRONG: Sequential — second request waits for first to finish
const user = await getUser(userId)
const posts = await getPosts(userId)
```

2. NEVER fetch in loops (N+1 problem):
```tsx
// ❌ N+1: Fires 100 queries
for (const id of userIds) {
  const user = await supabase.from('profiles').select().eq('id', id).single()
}

// ✅ Batch: Fires 1 query
const { data } = await supabase.from('profiles').select().in('id', userIds)
```

3. Use `unstable_cache()` for expensive server-side computations:
```tsx
import { unstable_cache } from 'next/cache'

const getCachedPosts = unstable_cache(
  async (userId: string) => {
    return supabase.from('posts').select().eq('user_id', userId)
  },
  ['user-posts'],
  { revalidate: 60, tags: ['posts'] }
)
```

## Bundle Size Rules
- Dynamic import heavy libraries: `const { Chart } = await import('chart.js')`
- Use `next/dynamic` for client components that don't need SSR:
```tsx
import dynamic from 'next/dynamic'
const RichTextEditor = dynamic(() => import('@/components/editor'), {
  ssr: false,
  loading: () => <div className="animate-pulse h-40 bg-muted rounded" />,
})
```
- NEVER import entire icon libraries: `import { Search } from 'lucide-react'` not `import * as icons`

## Image Optimization
- ALWAYS use `next/image` with explicit `width` and `height`
- Set `priority={true}` on above-the-fold hero images for LCP
- Use `placeholder="blur"` with `blurDataURL` for smooth loading
- Serve modern formats: Next.js automatically converts to WebP/AVIF

## Core Web Vitals Targets
- LCP (Largest Contentful Paint): < 2.5s
- FID (First Input Delay): < 100ms
- CLS (Cumulative Layout Shift): < 0.1
规则

Core project context — tech stack, folder structure, and global conventions

Core project context — tech stack, folder structure, and global conventions

# Vibe Stack — Project Context

## Tech Stack
- Framework: Next.js 15 (App Router) with React 19
- Language: TypeScript (strict mode)
- Styling: Tailwind CSS + shadcn/ui components
- Database: Supabase (PostgreSQL + RLS)
- Auth: Supabase SSR (cookie-based, @supabase/ssr)
- Validation: Zod
- Payments: Stripe (server-side only)
- Email: Resend
- Icons: lucide-react

## Architecture Rules
1. Default to Server Components. Only add 'use client' when the component needs interactivity.
2. Push 'use client' to the smallest leaf component possible.
3. All data fetching happens in Server Components or Server Actions.
4. All mutations happen via Server Actions, never client-side fetch().
5. All external input MUST be validated with Zod schemas.
6. Never use getSession() on the server — always use getUser().
7. All database tables MUST have Row Level Security (RLS) enabled.

## Folder Structure
```
src/
├── app/              # Next.js App Router pages & layouts
│   ├── (auth)/       # Auth route group (login, signup)
│   ├── dashboard/    # Protected pages (behind middleware)
│   └── api/          # Route handlers (webhooks, public APIs)
├── components/       # Reusable React components
│   ├── ui/           # shadcn/ui primitives
│   └── [feature]/    # Feature-specific components
├── lib/              # Utilities & third-party configs
│   ├── supabase/     # Supabase client factories  
│   ├── stripe/       # Stripe helpers (server-only)
│   └── utils.ts      # General utilities (cn helper)
└── types/            # Shared TypeScript types
```

## Naming Conventions
- Files: kebab-case (e.g., `user-profile.tsx`)
- Components: PascalCase (e.g., `UserProfile`)
- Server Actions: camelCase verbs (e.g., `createTodo`, `updateProfile`)
- Types/Interfaces: PascalCase with `interface` preferred over `type`
- Database tables: snake_case (e.g., `user_profiles`)
规则

Security rules and OWASP Top 10 enforcement for AI-generated code

Security rules and OWASP Top 10 enforcement for AI-generated code

# Security & OWASP Top 10

CRITICAL: AI models will generate insecure code by default. These rules are non-negotiable.

## 1. Injection Prevention (A03:2021)
NEVER use string interpolation in database queries.
```typescript
// ❌ SQL INJECTION VULNERABILITY
const { data } = await supabase.rpc('search', { query: `%${userInput}%` })

// ✅ SAFE: Use parameterized queries
const { data } = await supabase
  .from('items')
  .select()
  .ilike('name', `%${userInput}%`)  // Supabase auto-escapes
```

## 2. XSS Prevention (A07:2021)
```tsx
// ❌ XSS VULNERABILITY — NEVER do this
<div dangerouslySetInnerHTML={{ __html: userContent }} />

// ✅ If you MUST render HTML, sanitize first
import DOMPurify from 'isomorphic-dompurify'
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />
```

## 3. Authentication Boundary (A01:2021 - Broken Access Control)
EVERY Server Action and Route Handler MUST verify auth before ANY data operation:
```typescript
export async function deleteItem(id: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: 'Unauthorized' }

  // ALSO verify ownership — don't just check auth
  const { data: item } = await supabase
    .from('items')
    .select('user_id')
    .eq('id', id)
    .single()
  
  if (item?.user_id !== user.id) return { error: 'Forbidden' }

  await supabase.from('items').delete().eq('id', id)
}
```

## 4. Rate Limiting (A04:2021 - Insecure Design)
Sensitive endpoints MUST be rate limited:
```typescript
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(5, '60 s'), // 5 requests per minute
})

export async function POST(request: Request) {
  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'
  const { success } = await ratelimit.limit(ip)
  if (!success) {
    return Response.json({ error: 'Rate limited' }, { status: 429 })
  }
  // ... handle request
}
```

## 5. Environment Variable Safety
- API keys with write access: NEVER prefix with `NEXT_PUBLIC_`
- Supabase `service_role` key: SERVER ONLY — never in client bundles
- Stripe Secret Key: SERVER ONLY
- Only `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` are safe for the client

## 6. Input Validation
ALL user input MUST be validated with Zod before processing.
See `typescript-strict.mdc` for the full Zod pattern.

## 7. CORS
API routes consumed cross-origin MUST set explicit allowed origins:
```typescript
const headers = {
  'Access-Control-Allow-Origin': process.env.NEXT_PUBLIC_APP_URL!,
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
```
规则

Distinguishing between Server and Client Components

Distinguishing between Server and Client Components

# Server vs Client Components

Decision Tree (ALWAYS follow this order):
1. Can this component run as a Server Component? DEFAULT: YES
2. Does it use: `useState`, `useEffect`, `useRef`, event handlers, or browser APIs?
   → ONLY THEN add `'use client'`
3. Push `'use client'` to the smallest possible LEAF component

Optimization pattern:
- Server Component: data fetching, layout, most of the tree
- Client Component: only the interactive parts (button, form input, dropdown)

❌ Anti-pattern (Avoid):
```tsx
'use client'  
export default function ProductPage({ id }) {
  // Uses server fetch — should be Server Component
  const [product, setProduct] = useState(null)
  useEffect(() => { fetch(`/api/products/${id}`).then(...) }, [])
}
```

✅ Correct:
```tsx
// Server Component — no 'use client' needed for data fetching
export default async function ProductPage({ id }) {
  const product = await getProduct(id) 
}
```
规则

Shadcn/ui component usage patterns and anti-patterns

Shadcn/ui component usage patterns and anti-patterns

# Shadcn/ui Patterns

ALWAYS prefer shadcn/ui components over custom implementations.
Import from `@/components/ui/` — NEVER from external package names.

## Installation
Components are NOT installed from npm. Use the CLI:
```bash
npx shadcn@latest add button card dialog input
```
This copies source code into `src/components/ui/`. You OWN these files.

## Core Patterns

### Forms: ALWAYS use react-hook-form + Zod
```tsx
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'

const schema = z.object({ email: z.string().email() })

export function EmailForm() {
  const form = useForm({ resolver: zodResolver(schema) })
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl><Input {...field} /></FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? 'Sending...' : 'Submit'}
        </Button>
      </form>
    </Form>
  )
}
```

### Button Loading State
```tsx
<Button disabled={isPending}>
  {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
  {isPending ? 'Saving...' : 'Save Changes'}
</Button>
```

### Toast Notifications: Use Sonner
```tsx
import { toast } from 'sonner'
toast.success('Profile updated')
toast.error('Failed to save changes')
```

## Anti-Patterns — NEVER Do These
- NEVER create a custom Modal when `<Dialog>` exists
- NEVER create a custom Select when `<Select>` exists
- NEVER create a custom Tooltip when `<Tooltip>` exists
- NEVER use `alert()` or `window.confirm()` — use `<AlertDialog>`
- NEVER wrap shadcn components in unnecessary divs for styling — use `cn()` and className instead
规则

Stripe payment integration patterns — server-only, webhook handling

Stripe payment integration patterns — server-only, webhook handling

# Stripe Integration Rules

## RULE 1: Stripe is Server-Only
NEVER import `stripe` in a Client Component or expose the secret key.
All payment logic lives in Server Actions or Route Handlers.

## RULE 2: Use Checkout Sessions, Not Custom Forms
For subscriptions and one-time payments, always redirect to Stripe Checkout.
NEVER build a custom credit card form — it creates PCI compliance liability.

```typescript
'use server'
import { createCheckoutSession } from '@/lib/stripe'
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'

export async function subscribe(priceId: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) redirect('/login')

  const session = await createCheckoutSession({
    userId: user.id,
    userEmail: user.email!,
    priceId,
    successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?subscribed=true`,
    cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
  })

  redirect(session.url!)
}
```

## RULE 3: Webhook Signature Verification is Mandatory
The `/api/webhooks/stripe` route MUST verify the `stripe-signature` header
using `stripe.webhooks.constructEvent()`. Without this, attackers can send
fake events to grant themselves free subscriptions.

## RULE 4: Handle These Events at Minimum
- `checkout.session.completed` — activate subscription
- `customer.subscription.updated` — plan changes
- `customer.subscription.deleted` — cancellations
- `invoice.payment_failed` — mark as past_due
规则

Supabase auth security — enforces getUser() over getSession() on the server

Supabase auth security — enforces getUser() over getSession() on the server

# Supabase Auth Security

## RULE 1: NEVER Use getSession() on the Server

SECURITY CRITICAL: `getSession()` reads the JWT from cookies without revalidating it.
A malicious user can craft a fake session token that `getSession()` will silently accept.
`getUser()` sends a request to Supabase Auth to revalidate the JWT — it is cryptographically secure.

✅ SECURE (always use on the server):
```typescript
const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) redirect('/login')
```

❌ INSECURE (never use in server code):
```typescript
// VULNERABILITY: This does NOT verify the JWT with the auth server
const { data: { session } } = await supabase.auth.getSession()
```

## RULE 2: Always Use @supabase/ssr, Never @supabase/auth-helpers

The `@supabase/auth-helpers-nextjs` package is DEPRECATED.
Always import from `@supabase/ssr` for cookie-based sessions.

❌ DEPRECATED:
```typescript
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
```

✅ CORRECT:
```typescript
import { createBrowserClient } from '@supabase/ssr'    // Client Components
import { createServerClient } from '@supabase/ssr'      // Server Components
```

## RULE 3: Middleware Must Refresh Sessions
Every protected route requires middleware that calls `updateSession()`.
Without this, user tokens expire silently and server components see null users.

## RULE 4: PKCE Flow for Email Confirmations
When implementing email signup, create a callback route at `app/auth/confirm/route.ts` 
that exchanges the `token_hash` using `verifyOtp()`. The PKCE flow requires the user
to complete verification in the same browser session.
规则

Supabase Row Level Security enforcement

Supabase Row Level Security enforcement

# Supabase Row Level Security (RLS)

ALWAYS assume RLS is enabled. NEVER generate queries that assume admin access by default.

RLS Policy Patterns:
-- Own record only:
```sql
create policy "Users own data" on todos
  for all using (auth.uid() = user_id);
```

-- Read-only public:
```sql
create policy "Public read" on posts  
  for select using (published = true);
```

Code rules:
- ALWAYS use anon/authenticated clients (not `service_role`) for user operations
- Service role client: ONLY use in server-to-server operations (webhooks, cron jobs)
- NEVER pass `service_role` key to the browser
- Use `select('specific, columns')` not `select('*')` for performance
规则

Supabase SSR package enforcement and cookie handling

Supabase SSR package enforcement and cookie handling

# Supabase SSR Package Rules

You MUST use `@supabase/ssr` for all authentication.
You MUST NEVER import from `@supabase/auth-helpers-nextjs`.

The correct imports are:
```typescript
import { createServerClient } from '@supabase/ssr'        // Server components
import { createBrowserClient } from '@supabase/ssr'       // Client components
```

The cookie handling MUST use ONLY `getAll()` and `setAll()` in middleware or server clients:
```typescript
cookies: {
  getAll() { return cookieStore.getAll() },
  setAll(cookiesToSet) {
    cookiesToSet.forEach(({ name, value, options }) => 
      cookieStore.set(name, value, options))
  },
}
```
规则

Testing strategy for AI developers

Testing strategy for AI developers

# Testing Strategy

Use Vitest for unit tests, Playwright for E2E testing.

What to test (prioritized):
1. Server Actions (they're just functions — easy to test)
2. API route handlers
3. Auth flows (most critical)
4. Complex business logic and utility functions

What NOT to bother testing with unit tests:
- Simple React leaf components (visual testing is better)
- Styling logic and Tailwind classes
- Component boilerplate

Simple Server Action test example:
```typescript
import { createTodo } from '@/app/actions'
import { expect, test, describe } from 'vitest'

describe('createTodo Server Action', () => {
  test('creates todo for authenticated user', async () => {
    const formData = new FormData()
    formData.set('title', 'Test todo')
    
    const result = await createTodo(formData)
    expect(result.success).toBe(true)
  })
})
```
规则

Strict TypeScript boundaries — no any, Zod validation, explicit return types

Strict TypeScript boundaries — no any, Zod validation, explicit return types

# Strict TypeScript & Zod

## Core Rules
- `tsconfig.json` MUST have `strict: true`
- NEVER use `any` — use `unknown` and narrow with type guards
- ALWAYS provide explicit return types for Server Actions and API routes
- Prefer `interface` over `type` for public API contracts (better error messages, extendable)

## The `any` Ban
```typescript
// ❌ NEVER
function process(data: any) { return data.name }

// ✅ Use unknown + narrowing
function process(data: unknown): string {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    return (data as { name: string }).name
  }
  throw new Error('Invalid data shape')
}

// ✅ Or better — use Zod
const DataSchema = z.object({ name: z.string() })
function process(data: unknown): string {
  const parsed = DataSchema.parse(data)
  return parsed.name
}
```

## Zod at Every Boundary
Validate ALL external input at these boundaries:
1. **Server Actions** — `formData` from client
2. **Route Handlers** — `request.json()` from external callers
3. **Webhooks** — payload from Stripe, GitHub, etc.
4. **URL params** — `searchParams` from user-controlled URLs

```typescript
const CreateTodoSchema = z.object({
  title: z.string().min(1, 'Title is required').max(200),
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
  dueDate: z.string().datetime().optional(),
})

type CreateTodoInput = z.infer<typeof CreateTodoSchema>

export async function createTodo(formData: FormData): Promise<ActionResponse> {
  const parsed = CreateTodoSchema.safeParse({
    title: formData.get('title'),
    priority: formData.get('priority'),
    dueDate: formData.get('dueDate'),
  })

  if (!parsed.success) {
    return { success: false, error: parsed.error.issues[0].message }
  }

  // proceed with parsed.data (fully typed as CreateTodoInput)
}
```

## Utility Types You Should Use
- `Partial<T>` for optional updates
- `Pick<T, 'field1' | 'field2'>` for selecting fields
- `Omit<T, 'id' | 'created_at'>` for create inputs
- `Record<string, unknown>` instead of `object` or `{}`
- `NonNullable<T>` to strip null/undefined
规则

Guidelines for Server Actions — validation, error handling, and when to use Route Handlers instead

Guidelines for Server Actions — validation, error handling, and when to use Route Handlers instead

# Server Actions vs Route Handlers

## When to Use Server Actions
- Form submissions (POST with user data)
- Mutations triggered from UI (create, update, delete)
- Operations needing cookie/session access
- Revalidation of cached pages after a mutation

## RULE 1: Always Validate Inputs with Zod
NEVER trust raw FormData. Always parse through a Zod schema first.

✅ CORRECT:
```typescript
'use server'

import { z } from 'zod'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'

const createTodoSchema = z.object({
  title: z.string().min(1).max(200),
})

export async function createTodo(formData: FormData) {
  const parsed = createTodoSchema.safeParse({
    title: formData.get('title'),
  })

  if (!parsed.success) {
    return { error: 'Invalid input', details: parsed.error.flatten() }
  }

  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: 'Unauthorized' }

  const { error } = await supabase
    .from('todos')
    .insert({ title: parsed.data.title, user_id: user.id })

  if (error) return { error: error.message }

  revalidatePath('/todos')
  return { success: true }
}
```

## RULE 2: Return Structured Errors, Don't Throw
Server Actions should return `{ error: string }` or `{ success: true }`.
Only use `redirect()` for navigation after successful mutations.

## When to Use Route Handlers (app/api/**)
- Webhooks (Stripe, GitHub — external services calling your API)
- Public API endpoints (no auth needed or API key auth)
- File uploads/download streams
- Third-party OAuth callbacks
- CORS-required endpoints

## RULE 3: Always Add try/catch in Route Handlers
```typescript
export async function POST(request: Request) {
  try {
    const body = await request.json()
    // ... logic
    return Response.json({ success: true })
  } catch (error) {
    console.error('API Error:', error)
    return Response.json({ error: 'Internal server error' }, { status: 500 })
  }
}
```

来源:https://github.com/vibestackdev/vibe-stack